1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +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,
user: string,
): Promise<void> {
await this.dependentFeaturesStore.deleteAll(feature);
await this.dependentFeaturesStore.deleteAll([feature]);
await this.eventService.storeEvent({
type: 'feature-dependencies-removed',
project: projectId,

View File

@ -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>;
}

View File

@ -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();

View File

@ -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();

View File

@ -71,6 +71,7 @@ import {
import {
DATE_OPERATORS,
DEFAULT_ENV,
extractUsernameFromUser,
NUM_OPERATORS,
SEMVER_OPERATORS,
STRING_OPERATORS,
@ -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,
@ -1476,6 +1496,7 @@ class FeatureToggleService {
featureName,
projectId,
});
await this.validateNoOrphanParents([featureName]);
}
await this.validateNoChildren(featureName);
@ -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,
@ -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);
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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, {
@ -69,7 +71,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedX',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
@ -83,7 +85,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedY',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
@ -95,7 +97,7 @@ beforeAll(async () => {
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',

View File

@ -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);
@ -55,7 +57,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedX',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
@ -69,7 +71,7 @@ beforeAll(async () => {
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedY',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
@ -81,7 +83,7 @@ beforeAll(async () => {
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ',
'test',
testUser,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',

View File

@ -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);
@ -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);