Skip to content

Commit

Permalink
feat: Protect archive feature (#5003)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Oct 12, 2023
1 parent 2754c26 commit cfcf9de
Show file tree
Hide file tree
Showing 10 changed files with 69 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export class DependentFeaturesService {
projectId: string,
user: string,
): Promise<void> {
await this.dependentFeaturesStore.deleteAll(feature);
await this.dependentFeaturesStore.deleteAll([feature]);
await this.eventService.storeEvent({
type: 'feature-dependencies-removed',
project: projectId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { FeatureDependency, FeatureDependencyId } from './dependent-features';
export interface IDependentFeaturesStore {
upsert(featureDependency: FeatureDependency): Promise<void>;
delete(dependency: FeatureDependencyId): Promise<void>;
deleteAll(child?: string): Promise<void>;
deleteAll(children?: string[]): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
.del();
}

async deleteAll(feature: string | undefined): Promise<void> {
if (feature) {
async deleteAll(features: string[] | undefined): Promise<void> {
if (features) {
await this.db('dependent_features')
.andWhere('child', feature)
.whereIn('child', features)
.del();
} else {
await this.db('dependent_features').del();
Expand Down
3 changes: 1 addition & 2 deletions src/lib/features/feature-toggle/feature-toggle-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,10 +777,9 @@ export default class ProjectFeaturesController extends Controller {
res: Response<void>,
): Promise<void> {
const { featureName, projectId } = req.params;
const userName = extractUsername(req);
await this.featureService.archiveToggle(
featureName,
userName,
req.user,
projectId,
);
res.status(202).send();
Expand Down
58 changes: 49 additions & 9 deletions src/lib/features/feature-toggle/feature-toggle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
import {
DATE_OPERATORS,
DEFAULT_ENV,
extractUsernameFromUser,
NUM_OPERATORS,
SEMVER_OPERATORS,
STRING_OPERATORS,
Expand Down Expand Up @@ -1465,6 +1466,25 @@ class FeatureToggleService {
}

async archiveToggle(
featureName: string,
user: User,
projectId?: string,
): Promise<void> {
if (projectId) {
await this.stopWhenChangeRequestsEnabled(
projectId,
undefined,
user,
);
}
await this.unprotectedArchiveToggle(
featureName,
extractUsernameFromUser(user),
projectId,
);
}

async unprotectedArchiveToggle(
featureName: string,
createdBy: string,
projectId?: string,
Expand All @@ -1476,6 +1496,7 @@ class FeatureToggleService {
featureName,
projectId,
});
await this.validateNoOrphanParents([featureName]);
}

await this.validateNoChildren(featureName);
Expand All @@ -1492,12 +1513,27 @@ class FeatureToggleService {
}

async archiveToggles(
featureNames: string[],
user: User,
projectId: string,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(projectId, undefined, user);
await this.unprotectedArchiveToggles(
featureNames,
extractUsernameFromUser(user),
projectId,
);
}

async unprotectedArchiveToggles(
featureNames: string[],
createdBy: string,
projectId: string,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
await this.validateNoOrphanParents(featureNames);
await Promise.all([
this.validateFeaturesContext(featureNames, projectId),
this.validateNoOrphanParents(featureNames),
]);

const features = await this.featureToggleStore.getAllByNames(
featureNames,
Expand Down Expand Up @@ -2176,15 +2212,19 @@ class FeatureToggleService {

private async stopWhenChangeRequestsEnabled(
project: string,
environment: string,
environment?: string,
user?: User,
) {
const canBypass =
await this.changeRequestAccessReadModel.canBypassChangeRequest(
project,
environment,
user,
);
const canBypass = environment
? await this.changeRequestAccessReadModel.canBypassChangeRequest(
project,
environment,
user,
)
: await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(
project,
user,
);
if (!canBypass) {
throw new PermissionError(SKIP_CHANGE_REQUEST);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,8 @@ class FeatureController extends Controller {

async archiveToggle(req: IAuthRequest, res: Response): Promise<void> {
const { featureName } = req.params;
const userName = extractUsername(req);

await this.service.archiveToggle(featureName, userName);
await this.service.archiveToggle(featureName, req.user);
res.status(200).end();
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/lib/routes/admin-api/project/project-archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,8 @@ export default class ProjectArchiveController extends Controller {
): Promise<void> {
const { features } = req.body;
const { projectId } = req.params;
const userName = extractUsername(req);

await this.featureService.archiveToggles(features, userName, projectId);
await this.featureService.archiveToggles(features, req.user, projectId);
res.status(202).end();
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/test/e2e/api/client/feature.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
import User from '../../../../lib/types/user';

let app: IUnleashTest;
let db: ITestDb;
const testUser = { name: 'test' } as User;

beforeAll(async () => {
db = await dbInit('feature_api_client', getLogger, {
Expand Down Expand Up @@ -69,7 +71,7 @@ beforeAll(async () => {

await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedX',
'test',
testUser,
);

await app.services.featureToggleServiceV2.createFeatureToggle(
Expand All @@ -83,7 +85,7 @@ beforeAll(async () => {

await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedY',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
Expand All @@ -95,7 +97,7 @@ beforeAll(async () => {
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
Expand Down
8 changes: 5 additions & 3 deletions src/test/e2e/api/client/feature.optimal304.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import {
} from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import User from '../../../../lib/types/user';
// import { DEFAULT_ENV } from '../../../../lib/util/constants';

let app: IUnleashTest;
let db: ITestDb;
const testUser = { name: 'test' } as User;

beforeAll(async () => {
db = await dbInit('feature_304_api_client', getLogger);
Expand Down Expand Up @@ -55,7 +57,7 @@ beforeAll(async () => {

await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedX',
'test',
testUser,
);

await app.services.featureToggleServiceV2.createFeatureToggle(
Expand All @@ -69,7 +71,7 @@ beforeAll(async () => {

await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedY',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
Expand All @@ -81,7 +83,7 @@ beforeAll(async () => {
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
Expand Down
4 changes: 2 additions & 2 deletions src/test/e2e/services/project-service.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,7 @@ test('should only count active feature toggles for project', async () => {
enabled: false,
});

await featureToggleService.archiveToggle('only-active-t2', 'me');
await featureToggleService.archiveToggle('only-active-t2', user);

const projects = await projectService.getProjects();
const theProject = projects.find((p) => p.id === project.id);
Expand All @@ -1265,7 +1265,7 @@ test('should list projects with all features archived', async () => {
enabled: false,
});

await featureToggleService.archiveToggle('archived-toggle', 'me');
await featureToggleService.archiveToggle('archived-toggle', user);

const projects = await projectService.getProjects();
const theProject = projects.find((p) => p.id === project.id);
Expand Down

0 comments on commit cfcf9de

Please sign in to comment.