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:
parent
30d8444c80
commit
b4c8f92a26
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user