mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
feat: ability to disable custom strategies (#10885)
## About the changes This adds the ability to disable custom strategy creation and editing by using two env variables: `UNLEASH_DISABLE_CUSTOM_STRATEGY_CREATION` and `UNLEASH_DISABLE_CUSTOM_STRATEGY_EDITING`. Fixes: #9593 This could be useful if you want to remove the ability to create new custom strategies and rely on the built-in ones
This commit is contained in:
parent
a52ee10827
commit
bf19b62079
@ -29,6 +29,10 @@ exports[`should create default config 1`] = `
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"maxAge": 3600000,
|
"maxAge": 3600000,
|
||||||
},
|
},
|
||||||
|
"customStrategySettings": {
|
||||||
|
"disableCreation": false,
|
||||||
|
"disableEditing": false,
|
||||||
|
},
|
||||||
"dailyMetricsStorageDays": 91,
|
"dailyMetricsStorageDays": 91,
|
||||||
"db": {
|
"db": {
|
||||||
"acquireConnectionTimeout": 30000,
|
"acquireConnectionTimeout": 30000,
|
||||||
|
|||||||
@ -585,6 +585,18 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
).values(),
|
).values(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const customStrategySettings = options.customStrategySettings ?? {
|
||||||
|
disableCreation: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_DISABLE_CUSTOM_STRATEGY_CREATION,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
|
||||||
|
disableEditing: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_DISABLE_CUSTOM_STRATEGY_EDITING,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
const environmentEnableOverrides = loadEnvironmentEnableOverrides();
|
const environmentEnableOverrides = loadEnvironmentEnableOverrides();
|
||||||
|
|
||||||
const importSetting: IImportOption = mergeAll([
|
const importSetting: IImportOption = mergeAll([
|
||||||
@ -834,6 +846,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
userInactivityThresholdInDays,
|
userInactivityThresholdInDays,
|
||||||
buildDate: process.env.BUILD_DATE,
|
buildDate: process.env.BUILD_DATE,
|
||||||
unleashFrontendToken,
|
unleashFrontendToken,
|
||||||
|
customStrategySettings,
|
||||||
checkDbOnReady,
|
checkDbOnReady,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import createStores from '../../../test/fixtures/store.js';
|
|||||||
import permissions from '../../../test/fixtures/permissions.js';
|
import permissions from '../../../test/fixtures/permissions.js';
|
||||||
import getApp from '../../app.js';
|
import getApp from '../../app.js';
|
||||||
import { createServices } from '../../services/index.js';
|
import { createServices } from '../../services/index.js';
|
||||||
import { vi } from 'vitest';
|
import { afterEach, vi } from 'vitest';
|
||||||
|
|
||||||
async function getSetup() {
|
async function getSetup() {
|
||||||
const randomBase = `/random${Math.round(Math.random() * 1000)}`;
|
const randomBase = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
@ -25,6 +25,11 @@ async function getSetup() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env.UNLEASH_DISABLE_CUSTOM_STRATEGY_CREATION;
|
||||||
|
delete process.env.UNLEASH_DISABLE_CUSTOM_STRATEGY_EDITING;
|
||||||
|
});
|
||||||
|
|
||||||
test('add version numbers for /strategies', async () => {
|
test('add version numbers for /strategies', async () => {
|
||||||
const { request, base } = await getSetup();
|
const { request, base } = await getSetup();
|
||||||
return request
|
return request
|
||||||
@ -71,6 +76,20 @@ test('create a new strategy with empty parameters', async () => {
|
|||||||
.expect(201);
|
.expect(201);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('creating strategies is forbidden when disabled by configuration', async () => {
|
||||||
|
process.env.UNLEASH_DISABLE_CUSTOM_STRATEGY_CREATION = 'true';
|
||||||
|
const { request, base } = await getSetup();
|
||||||
|
|
||||||
|
const response = await request
|
||||||
|
.post(`${base}/api/admin/strategies`)
|
||||||
|
.send({ name: 'LockedStrategy', parameters: [] })
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(response.body.message).toContain(
|
||||||
|
'Custom strategy creation is disabled',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('not be possible to override name', async () => {
|
test('not be possible to override name', async () => {
|
||||||
const { request, base, strategyStore } = await getSetup();
|
const { request, base, strategyStore } = await getSetup();
|
||||||
strategyStore.createStrategy({ name: 'Testing', parameters: [] });
|
strategyStore.createStrategy({ name: 'Testing', parameters: [] });
|
||||||
@ -92,6 +111,22 @@ test('update strategy', async () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('updating strategies is forbidden when disabled by configuration', async () => {
|
||||||
|
process.env.UNLEASH_DISABLE_CUSTOM_STRATEGY_EDITING = 'true';
|
||||||
|
const { request, base, strategyStore } = await getSetup();
|
||||||
|
const name = 'LockedStrategy';
|
||||||
|
await strategyStore.createStrategy({ name, parameters: [] });
|
||||||
|
|
||||||
|
const response = await request
|
||||||
|
.put(`${base}/api/admin/strategies/${name}`)
|
||||||
|
.send({ name, parameters: [] })
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(response.body.message).toContain(
|
||||||
|
'Custom strategy modification is disabled',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('not update unknown strategy', async () => {
|
test('not update unknown strategy', async () => {
|
||||||
const { request, base } = await getSetup();
|
const { request, base } = await getSetup();
|
||||||
const name = 'UnknownStrat';
|
const name = 'UnknownStrat';
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
StrategyUpdatedEvent,
|
StrategyUpdatedEvent,
|
||||||
} from '../types/index.js';
|
} from '../types/index.js';
|
||||||
import strategySchema from './strategy-schema.js';
|
import strategySchema from './strategy-schema.js';
|
||||||
import { NameExistsError } from '../error/index.js';
|
import { NameExistsError, OperationDeniedError } from '../error/index.js';
|
||||||
|
|
||||||
class StrategyService {
|
class StrategyService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -26,14 +26,20 @@ class StrategyService {
|
|||||||
|
|
||||||
private eventService: EventService;
|
private eventService: EventService;
|
||||||
|
|
||||||
|
private customStrategySettings: IUnleashConfig['customStrategySettings'];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{ strategyStore }: Pick<IUnleashStores, 'strategyStore'>,
|
{ strategyStore }: Pick<IUnleashStores, 'strategyStore'>,
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
{
|
||||||
|
getLogger,
|
||||||
|
customStrategySettings,
|
||||||
|
}: Pick<IUnleashConfig, 'getLogger' | 'customStrategySettings'>,
|
||||||
eventService: EventService,
|
eventService: EventService,
|
||||||
) {
|
) {
|
||||||
this.strategyStore = strategyStore;
|
this.strategyStore = strategyStore;
|
||||||
this.eventService = eventService;
|
this.eventService = eventService;
|
||||||
this.logger = getLogger('services/strategy-service.js');
|
this.logger = getLogger('services/strategy-service.js');
|
||||||
|
this.customStrategySettings = customStrategySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStrategies(): Promise<IStrategy[]> {
|
async getStrategies(): Promise<IStrategy[]> {
|
||||||
@ -103,6 +109,7 @@ class StrategyService {
|
|||||||
value: IMinimalStrategy,
|
value: IMinimalStrategy,
|
||||||
auditUser: IAuditUser,
|
auditUser: IAuditUser,
|
||||||
): Promise<IStrategy | undefined> {
|
): Promise<IStrategy | undefined> {
|
||||||
|
this.assertCreationAllowed();
|
||||||
const strategy = await strategySchema.validateAsync(value);
|
const strategy = await strategySchema.validateAsync(value);
|
||||||
strategy.deprecated = false;
|
strategy.deprecated = false;
|
||||||
await this._validateStrategyName(strategy);
|
await this._validateStrategyName(strategy);
|
||||||
@ -120,6 +127,7 @@ class StrategyService {
|
|||||||
input: IMinimalStrategy,
|
input: IMinimalStrategy,
|
||||||
auditUser: IAuditUser,
|
auditUser: IAuditUser,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
this.assertEditingAllowed();
|
||||||
const value = await strategySchema.validateAsync(input);
|
const value = await strategySchema.validateAsync(input);
|
||||||
const strategy = await this.strategyStore.get(input.name);
|
const strategy = await this.strategyStore.get(input.name);
|
||||||
await this._validateEditable(strategy);
|
await this._validateEditable(strategy);
|
||||||
@ -155,5 +163,21 @@ class StrategyService {
|
|||||||
throw new Error(`Cannot edit strategy ${strategy?.name}`);
|
throw new Error(`Cannot edit strategy ${strategy?.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private assertCreationAllowed(): void {
|
||||||
|
if (this.customStrategySettings?.disableCreation) {
|
||||||
|
throw new OperationDeniedError(
|
||||||
|
'Custom strategy creation is disabled by configuration.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertEditingAllowed(): void {
|
||||||
|
if (this.customStrategySettings?.disableEditing) {
|
||||||
|
throw new OperationDeniedError(
|
||||||
|
'Custom strategy modification is disabled by configuration.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default StrategyService;
|
export default StrategyService;
|
||||||
|
|||||||
@ -118,6 +118,11 @@ export interface IClientCachingOption {
|
|||||||
maxAge: number;
|
maxAge: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICustomStrategySettings {
|
||||||
|
disableCreation: boolean;
|
||||||
|
disableEditing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResourceLimits {
|
export interface ResourceLimits {
|
||||||
apiTokens: number;
|
apiTokens: number;
|
||||||
constraints: number;
|
constraints: number;
|
||||||
@ -175,6 +180,7 @@ export interface IUnleashOptions {
|
|||||||
resourceLimits?: Partial<ResourceLimits>;
|
resourceLimits?: Partial<ResourceLimits>;
|
||||||
userInactivityThresholdInDays?: number;
|
userInactivityThresholdInDays?: number;
|
||||||
unleashFrontendToken?: string;
|
unleashFrontendToken?: string;
|
||||||
|
customStrategySettings?: ICustomStrategySettings;
|
||||||
checkDbOnReady?: boolean;
|
checkDbOnReady?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,5 +308,6 @@ export interface IUnleashConfig {
|
|||||||
userInactivityThresholdInDays: number;
|
userInactivityThresholdInDays: number;
|
||||||
buildDate?: string;
|
buildDate?: string;
|
||||||
unleashFrontendToken?: string;
|
unleashFrontendToken?: string;
|
||||||
|
customStrategySettings?: ICustomStrategySettings;
|
||||||
checkDbOnReady?: boolean;
|
checkDbOnReady?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user