diff --git a/frontend/src/component/providers/AccessProvider/permissions.ts b/frontend/src/component/providers/AccessProvider/permissions.ts index 1815f0eb19..1e7ee2c0c4 100644 --- a/frontend/src/component/providers/AccessProvider/permissions.ts +++ b/frontend/src/component/providers/AccessProvider/permissions.ts @@ -35,3 +35,4 @@ export const UPDATE_SEGMENT = 'UPDATE_SEGMENT'; export const DELETE_SEGMENT = 'DELETE_SEGMENT'; export const APPLY_CHANGE_REQUEST = 'APPLY_CHANGE_REQUEST'; export const APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST'; +export const SKIP_CHANGE_REQUEST = 'SKIP_CHANGE_REQUEST'; diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index ecc7e9ec44..8e1aa4a327 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -273,6 +273,7 @@ class FeatureController extends Controller { environment: DEFAULT_ENV, }, userName, + req.user, ), ), ); @@ -319,6 +320,7 @@ class FeatureController extends Controller { s, { projectId, featureName, environment: DEFAULT_ENV }, userName, + req.user, ), ), ); diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index 51139fd20e..a4e9d3a0d1 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -618,6 +618,7 @@ export default class ProjectFeaturesController extends Controller { strategyConfig, { environment, projectId, featureName }, userName, + req.user, ); const updatedStrategy = await this.featureService.getStrategy( @@ -674,6 +675,7 @@ export default class ProjectFeaturesController extends Controller { req.body, { environment, projectId, featureName }, userName, + req.user, ); res.status(200).json(updatedStrategy); } @@ -692,6 +694,7 @@ export default class ProjectFeaturesController extends Controller { newDocument, { environment, projectId, featureName }, userName, + req.user, ); res.status(200).json(updatedStrategy); } @@ -720,6 +723,7 @@ export default class ProjectFeaturesController extends Controller { strategyId, { environment, projectId, featureName }, userName, + req.user, ); res.status(200).json(strategy); } diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index 2683ae8465..98dba11637 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -76,7 +76,10 @@ import { SetStrategySortOrderSchema } from 'lib/openapi/spec/set-strategy-sort-o import { getDefaultStrategy } from '../util/feature-evaluator/helpers'; import { AccessService } from './access-service'; import { User } from '../server-impl'; -import { CREATE_FEATURE_STRATEGY } from '../types/permissions'; +import { + CREATE_FEATURE_STRATEGY, + SKIP_CHANGE_REQUEST, +} from '../types/permissions'; import NoAccessError from '../error/no-access-error'; import { IFeatureProjectUserParams } from '../routes/admin-api/project/features'; @@ -321,10 +324,12 @@ class FeatureToggleService { strategyConfig: Unsaved, context: IFeatureStrategyContext, createdBy: string, + user?: User, ): Promise> { await this.stopWhenChangeRequestsEnabled( context.projectId, context.environment, + user, ); return this.unprotectedCreateStrategy( strategyConfig, @@ -413,10 +418,12 @@ class FeatureToggleService { updates: Partial, context: IFeatureStrategyContext, userName: string, + user?: User, ): Promise> { await this.stopWhenChangeRequestsEnabled( context.projectId, context.environment, + user, ); return this.unprotectedUpdateStrategy(id, updates, context, userName); } @@ -533,10 +540,12 @@ class FeatureToggleService { id: string, context: IFeatureStrategyContext, createdBy: string, + user?: User, ): Promise { await this.stopWhenChangeRequestsEnabled( context.projectId, context.environment, + user, ); return this.unprotectedDeleteStrategy(id, context, createdBy); } @@ -1014,7 +1023,7 @@ class FeatureToggleService { createdBy: string, user?: User, ): Promise { - await this.stopWhenChangeRequestsEnabled(project, environment); + await this.stopWhenChangeRequestsEnabled(project, environment, user); if (enabled) { await this.stopWhenCannotCreateStrategies( project, @@ -1375,16 +1384,21 @@ class FeatureToggleService { private async stopWhenChangeRequestsEnabled( project: string, environment: string, + user?: User, ) { - if ( - await this.accessService.isChangeRequestsEnabled( - project, - environment, - ) - ) { - throw new Error( - `Change requests are enabled in project "${project}" for environment "${environment}"`, - ); + const [canSkipChangeRequest, changeRequestEnabled] = await Promise.all([ + user + ? this.accessService.hasPermission( + user, + SKIP_CHANGE_REQUEST, + project, + environment, + ) + : Promise.resolve(false), + this.accessService.isChangeRequestsEnabled(project, environment), + ]); + if (changeRequestEnabled && !canSkipChangeRequest) { + throw new NoAccessError(SKIP_CHANGE_REQUEST); } } diff --git a/src/lib/types/permissions.ts b/src/lib/types/permissions.ts index fea077d7a5..00b3a98b6c 100644 --- a/src/lib/types/permissions.ts +++ b/src/lib/types/permissions.ts @@ -41,3 +41,4 @@ export const UPDATE_SEGMENT = 'UPDATE_SEGMENT'; export const DELETE_SEGMENT = 'DELETE_SEGMENT'; export const APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST'; export const APPLY_CHANGE_REQUEST = 'APPLY_CHANGE_REQUEST'; +export const SKIP_CHANGE_REQUEST = 'SKIP_CHANGE_REQUEST'; diff --git a/src/migrations/20221205122253-skip-change-request.js b/src/migrations/20221205122253-skip-change-request.js new file mode 100644 index 0000000000..95ffaad5cc --- /dev/null +++ b/src/migrations/20221205122253-skip-change-request.js @@ -0,0 +1,17 @@ +exports.up = function (db, cb) { + db.runSql( + ` + INSERT INTO permissions (permission, display_name, type) VALUES ('SKIP_CHANGE_REQUEST', 'Skip change request process', 'environment'); + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + DELETE FROM permissions WHERE permission = 'SKIP_CHANGE_REQUEST'; + `, + cb, + ); +};