1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

feat: do not allow to manage dependencies directly with cr enabled (#4971)

This commit is contained in:
Mateusz Kwasniewski 2023-10-10 09:25:03 +02:00 committed by GitHub
parent 30d8444c80
commit b4c8f92a26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 83 additions and 10 deletions

View File

@ -76,7 +76,7 @@ const useManageDependency = (
useDependentFeaturesApi(project); useDependentFeaturesApi(project);
const handleAddChange = async ( const handleAddChange = async (
actionType: 'addDependency' | 'deleteDependencies', actionType: 'addDependency' | 'deleteDependency',
) => { ) => {
if (!environment) { if (!environment) {
console.error('No change request environment'); console.error('No change request environment');
@ -91,7 +91,7 @@ const useManageDependency = (
}, },
]); ]);
} }
if (actionType === 'deleteDependencies') { if (actionType === 'deleteDependency') {
await addChange(project, environment, [ await addChange(project, environment, [
{ action: actionType, feature: featureId, payload: undefined }, { action: actionType, feature: featureId, payload: undefined },
]); ]);
@ -112,7 +112,7 @@ const useManageDependency = (
if (isChangeRequestConfiguredInAnyEnv()) { if (isChangeRequestConfiguredInAnyEnv()) {
await handleAddChange( await handleAddChange(
parent === REMOVE_DEPENDENCY_OPTION.key parent === REMOVE_DEPENDENCY_OPTION.key
? 'deleteDependencies' ? 'deleteDependency'
: 'addDependency', : 'addDependency',
); );
} else if (parent === REMOVE_DEPENDENCY_OPTION.key) { } else if (parent === REMOVE_DEPENDENCY_OPTION.key) {

View File

@ -13,7 +13,7 @@ export interface IChangeSchema {
| 'archiveFeature' | 'archiveFeature'
| 'updateSegment' | 'updateSegment'
| 'addDependency' | 'addDependency'
| 'deleteDependencies'; | 'deleteDependency';
payload: string | boolean | object | number | undefined; payload: string | boolean | object | number | undefined;
} }

View File

@ -10,6 +10,10 @@ import { EventService } from '../../services';
import FeatureTagStore from '../../db/feature-tag-store'; import FeatureTagStore from '../../db/feature-tag-store';
import FakeEventStore from '../../../test/fixtures/fake-event-store'; import FakeEventStore from '../../../test/fixtures/fake-event-store';
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store'; import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
import {
createChangeRequestAccessReadModel,
createFakeChangeRequestAccessService,
} from '../change-request-access-service/createChangeRequestAccessReadModel';
export const createDependentFeaturesService = ( export const createDependentFeaturesService = (
db: Db, db: Db,
@ -27,9 +31,14 @@ export const createDependentFeaturesService = (
); );
const dependentFeaturesStore = new DependentFeaturesStore(db); const dependentFeaturesStore = new DependentFeaturesStore(db);
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db); const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
const changeRequestAccessReadModel = createChangeRequestAccessReadModel(
db,
config,
);
return new DependentFeaturesService( return new DependentFeaturesService(
dependentFeaturesStore, dependentFeaturesStore,
dependentFeaturesReadModel, dependentFeaturesReadModel,
changeRequestAccessReadModel,
eventService, eventService,
); );
}; };
@ -48,9 +57,12 @@ export const createFakeDependentFeaturesService = (
); );
const dependentFeaturesStore = new FakeDependentFeaturesStore(); const dependentFeaturesStore = new FakeDependentFeaturesStore();
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel(); const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
return new DependentFeaturesService( return new DependentFeaturesService(
dependentFeaturesStore, dependentFeaturesStore,
dependentFeaturesReadModel, dependentFeaturesReadModel,
changeRequestAccessReadModel,
eventService, eventService,
); );
}; };

View File

@ -186,7 +186,7 @@ export default class DependentFeaturesController extends Controller {
enabled, enabled,
feature, feature,
}, },
extractUsernameFromUser(req.user), req.user,
), ),
); );
res.status(200).end(); res.status(200).end();
@ -210,7 +210,7 @@ export default class DependentFeaturesController extends Controller {
child, child,
}, },
projectId, projectId,
extractUsernameFromUser(req.user), req.user,
); );
res.status(200).end(); res.status(200).end();
} else { } else {
@ -230,7 +230,7 @@ export default class DependentFeaturesController extends Controller {
await this.dependentFeaturesService.deleteFeatureDependencies( await this.dependentFeaturesService.deleteFeatureDependencies(
child, child,
projectId, projectId,
extractUsernameFromUser(req.user), req.user,
); );
res.status(200).end(); res.status(200).end();
} else { } else {

View File

@ -1,24 +1,32 @@
import { InvalidOperationError } from '../../error'; import { InvalidOperationError, PermissionError } from '../../error';
import { CreateDependentFeatureSchema } from '../../openapi'; import { CreateDependentFeatureSchema } from '../../openapi';
import { IDependentFeaturesStore } from './dependent-features-store-type'; import { IDependentFeaturesStore } from './dependent-features-store-type';
import { FeatureDependency, FeatureDependencyId } from './dependent-features'; import { FeatureDependency, FeatureDependencyId } from './dependent-features';
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type'; import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { User } from '../../server-impl';
import { SKIP_CHANGE_REQUEST } from '../../types';
import { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
import { extractUsernameFromUser } from '../../util';
export class DependentFeaturesService { export class DependentFeaturesService {
private dependentFeaturesStore: IDependentFeaturesStore; private dependentFeaturesStore: IDependentFeaturesStore;
private dependentFeaturesReadModel: IDependentFeaturesReadModel; private dependentFeaturesReadModel: IDependentFeaturesReadModel;
private changeRequestAccessReadModel: IChangeRequestAccessReadModel;
private eventService: EventService; private eventService: EventService;
constructor( constructor(
dependentFeaturesStore: IDependentFeaturesStore, dependentFeaturesStore: IDependentFeaturesStore,
dependentFeaturesReadModel: IDependentFeaturesReadModel, dependentFeaturesReadModel: IDependentFeaturesReadModel,
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
eventService: EventService, eventService: EventService,
) { ) {
this.dependentFeaturesStore = dependentFeaturesStore; this.dependentFeaturesStore = dependentFeaturesStore;
this.dependentFeaturesReadModel = dependentFeaturesReadModel; this.dependentFeaturesReadModel = dependentFeaturesReadModel;
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.eventService = eventService; this.eventService = eventService;
} }
@ -35,7 +43,7 @@ export class DependentFeaturesService {
); );
await Promise.all( await Promise.all(
parents.map((parent) => parents.map((parent) =>
this.upsertFeatureDependency( this.unprotectedUpsertFeatureDependency(
{ child: newFeatureName, projectId }, { child: newFeatureName, projectId },
{ {
feature: parent.feature, feature: parent.feature,
@ -49,6 +57,20 @@ export class DependentFeaturesService {
} }
async upsertFeatureDependency( async upsertFeatureDependency(
{ child, projectId }: { child: string; projectId: string },
dependentFeature: CreateDependentFeatureSchema,
user: User,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(projectId, user);
return this.unprotectedUpsertFeatureDependency(
{ child, projectId },
dependentFeature,
extractUsernameFromUser(user),
);
}
async unprotectedUpsertFeatureDependency(
{ child, projectId }: { child: string; projectId: string }, { child, projectId }: { child: string; projectId: string },
dependentFeature: CreateDependentFeatureSchema, dependentFeature: CreateDependentFeatureSchema,
user: string, user: string,
@ -92,6 +114,20 @@ export class DependentFeaturesService {
} }
async deleteFeatureDependency( async deleteFeatureDependency(
dependency: FeatureDependencyId,
projectId: string,
user: User,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(projectId, user);
return this.unprotectedDeleteFeatureDependency(
dependency,
projectId,
extractUsernameFromUser(user),
);
}
async unprotectedDeleteFeatureDependency(
dependency: FeatureDependencyId, dependency: FeatureDependencyId,
projectId: string, projectId: string,
user: string, user: string,
@ -107,6 +143,20 @@ export class DependentFeaturesService {
} }
async deleteFeatureDependencies( async deleteFeatureDependencies(
feature: string,
projectId: string,
user: User,
): Promise<void> {
await this.stopWhenChangeRequestsEnabled(projectId, user);
return this.unprotectedDeleteFeatureDependencies(
feature,
projectId,
extractUsernameFromUser(user),
);
}
async unprotectedDeleteFeatureDependencies(
feature: string, feature: string,
projectId: string, projectId: string,
user: string, user: string,
@ -123,4 +173,15 @@ export class DependentFeaturesService {
async getParentOptions(feature: string): Promise<string[]> { async getParentOptions(feature: string): Promise<string[]> {
return this.dependentFeaturesReadModel.getParentOptions(feature); return this.dependentFeaturesReadModel.getParentOptions(feature);
} }
private async stopWhenChangeRequestsEnabled(project: string, user?: User) {
const canBypass =
await this.changeRequestAccessReadModel.canBypassChangeRequestForProject(
project,
user,
);
if (!canBypass) {
throw new PermissionError(SKIP_CHANGE_REQUEST);
}
}
} }

View File

@ -61,7 +61,7 @@ beforeAll(async () => {
'test', 'test',
); );
// depend on enabled feature with variant // depend on enabled feature with variant
await app.services.dependentFeaturesService.upsertFeatureDependency( await app.services.dependentFeaturesService.unprotectedUpsertFeatureDependency(
{ child: 'featureY', projectId: 'default' }, { child: 'featureY', projectId: 'default' },
{ feature: 'featureX', variants: ['featureXVariant'] }, { feature: 'featureX', variants: ['featureXVariant'] },
'test', 'test',