1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

feat: Protect archive feature (#5003)

This commit is contained in:
Mateusz Kwasniewski 2023-10-12 08:38:03 +02:00 committed by GitHub
parent 2754c26f2e
commit cfcf9de65a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 69 additions and 28 deletions

View File

@ -182,7 +182,7 @@ export class DependentFeaturesService {
projectId: string, projectId: string,
user: string, user: string,
): Promise<void> { ): Promise<void> {
await this.dependentFeaturesStore.deleteAll(feature); await this.dependentFeaturesStore.deleteAll([feature]);
await this.eventService.storeEvent({ await this.eventService.storeEvent({
type: 'feature-dependencies-removed', type: 'feature-dependencies-removed',
project: projectId, project: projectId,

View File

@ -3,5 +3,5 @@ import { FeatureDependency, FeatureDependencyId } from './dependent-features';
export interface IDependentFeaturesStore { export interface IDependentFeaturesStore {
upsert(featureDependency: FeatureDependency): Promise<void>; upsert(featureDependency: FeatureDependency): Promise<void>;
delete(dependency: FeatureDependencyId): Promise<void>; delete(dependency: FeatureDependencyId): Promise<void>;
deleteAll(child?: string): Promise<void>; deleteAll(children?: string[]): Promise<void>;
} }

View File

@ -41,10 +41,10 @@ export class DependentFeaturesStore implements IDependentFeaturesStore {
.del(); .del();
} }
async deleteAll(feature: string | undefined): Promise<void> { async deleteAll(features: string[] | undefined): Promise<void> {
if (feature) { if (features) {
await this.db('dependent_features') await this.db('dependent_features')
.andWhere('child', feature) .whereIn('child', features)
.del(); .del();
} else { } else {
await this.db('dependent_features').del(); await this.db('dependent_features').del();

View File

@ -777,10 +777,9 @@ export default class ProjectFeaturesController extends Controller {
res: Response<void>, res: Response<void>,
): Promise<void> { ): Promise<void> {
const { featureName, projectId } = req.params; const { featureName, projectId } = req.params;
const userName = extractUsername(req);
await this.featureService.archiveToggle( await this.featureService.archiveToggle(
featureName, featureName,
userName, req.user,
projectId, projectId,
); );
res.status(202).send(); res.status(202).send();

View File

@ -71,6 +71,7 @@ import {
import { import {
DATE_OPERATORS, DATE_OPERATORS,
DEFAULT_ENV, DEFAULT_ENV,
extractUsernameFromUser,
NUM_OPERATORS, NUM_OPERATORS,
SEMVER_OPERATORS, SEMVER_OPERATORS,
STRING_OPERATORS, STRING_OPERATORS,
@ -1465,6 +1466,25 @@ class FeatureToggleService {
} }
async archiveToggle( 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, featureName: string,
createdBy: string, createdBy: string,
projectId?: string, projectId?: string,
@ -1476,6 +1496,7 @@ class FeatureToggleService {
featureName, featureName,
projectId, projectId,
}); });
await this.validateNoOrphanParents([featureName]);
} }
await this.validateNoChildren(featureName); await this.validateNoChildren(featureName);
@ -1492,12 +1513,27 @@ class FeatureToggleService {
} }
async archiveToggles( 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[], featureNames: string[],
createdBy: string, createdBy: string,
projectId: string, projectId: string,
): Promise<void> { ): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId); await Promise.all([
await this.validateNoOrphanParents(featureNames); this.validateFeaturesContext(featureNames, projectId),
this.validateNoOrphanParents(featureNames),
]);
const features = await this.featureToggleStore.getAllByNames( const features = await this.featureToggleStore.getAllByNames(
featureNames, featureNames,
@ -2176,15 +2212,19 @@ class FeatureToggleService {
private async stopWhenChangeRequestsEnabled( private async stopWhenChangeRequestsEnabled(
project: string, project: string,
environment: string, environment?: string,
user?: User, user?: User,
) { ) {
const canBypass = const canBypass = environment
await this.changeRequestAccessReadModel.canBypassChangeRequest( ? await this.changeRequestAccessReadModel.canBypassChangeRequest(
project, project,
environment, environment,
user, user,
); )
: await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(
project,
user,
);
if (!canBypass) { if (!canBypass) {
throw new PermissionError(SKIP_CHANGE_REQUEST); throw new PermissionError(SKIP_CHANGE_REQUEST);
} }

View File

@ -489,9 +489,8 @@ class FeatureController extends Controller {
async archiveToggle(req: IAuthRequest, res: Response): Promise<void> { async archiveToggle(req: IAuthRequest, res: Response): Promise<void> {
const { featureName } = req.params; 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(); res.status(200).end();
} }
} }

View File

@ -140,9 +140,8 @@ export default class ProjectArchiveController extends Controller {
): Promise<void> { ): Promise<void> {
const { features } = req.body; const { features } = req.body;
const { projectId } = req.params; 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(); res.status(202).end();
} }
} }

View File

@ -5,9 +5,11 @@ import {
import dbInit, { ITestDb } from '../../helpers/database-init'; import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants'; import { DEFAULT_ENV } from '../../../../lib/util/constants';
import User from '../../../../lib/types/user';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
const testUser = { name: 'test' } as User;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('feature_api_client', getLogger, { db = await dbInit('feature_api_client', getLogger, {
@ -69,7 +71,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle( await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedX', 'featureArchivedX',
'test', testUser,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
@ -83,7 +85,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle( await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedY', 'featureArchivedY',
'test', testUser,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
'default', 'default',
@ -95,7 +97,7 @@ beforeAll(async () => {
); );
await app.services.featureToggleServiceV2.archiveToggle( await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ', 'featureArchivedZ',
'test', testUser,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
'default', 'default',

View File

@ -4,10 +4,12 @@ import {
} from '../../helpers/test-helper'; } from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init'; import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import User from '../../../../lib/types/user';
// import { DEFAULT_ENV } from '../../../../lib/util/constants'; // import { DEFAULT_ENV } from '../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
const testUser = { name: 'test' } as User;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('feature_304_api_client', getLogger); db = await dbInit('feature_304_api_client', getLogger);
@ -55,7 +57,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle( await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedX', 'featureArchivedX',
'test', testUser,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
@ -69,7 +71,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle( await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedY', 'featureArchivedY',
'test', testUser,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
'default', 'default',
@ -81,7 +83,7 @@ beforeAll(async () => {
); );
await app.services.featureToggleServiceV2.archiveToggle( await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ', 'featureArchivedZ',
'test', testUser,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
'default', 'default',

View File

@ -1241,7 +1241,7 @@ test('should only count active feature toggles for project', async () => {
enabled: false, enabled: false,
}); });
await featureToggleService.archiveToggle('only-active-t2', 'me'); await featureToggleService.archiveToggle('only-active-t2', user);
const projects = await projectService.getProjects(); const projects = await projectService.getProjects();
const theProject = projects.find((p) => p.id === project.id); const theProject = projects.find((p) => p.id === project.id);
@ -1265,7 +1265,7 @@ test('should list projects with all features archived', async () => {
enabled: false, enabled: false,
}); });
await featureToggleService.archiveToggle('archived-toggle', 'me'); await featureToggleService.archiveToggle('archived-toggle', user);
const projects = await projectService.getProjects(); const projects = await projectService.getProjects();
const theProject = projects.find((p) => p.id === project.id); const theProject = projects.find((p) => p.id === project.id);