1
0
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:
Gastón Fournier 2025-11-03 09:27:15 +01:00 committed by GitHub
parent a52ee10827
commit bf19b62079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 86 additions and 3 deletions

View File

@ -29,6 +29,10 @@ exports[`should create default config 1`] = `
"enabled": true,
"maxAge": 3600000,
},
"customStrategySettings": {
"disableCreation": false,
"disableEditing": false,
},
"dailyMetricsStorageDays": 91,
"db": {
"acquireConnectionTimeout": 30000,

View File

@ -585,6 +585,18 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
).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 importSetting: IImportOption = mergeAll([
@ -834,6 +846,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
userInactivityThresholdInDays,
buildDate: process.env.BUILD_DATE,
unleashFrontendToken,
customStrategySettings,
checkDbOnReady,
};
}

View File

@ -4,7 +4,7 @@ import createStores from '../../../test/fixtures/store.js';
import permissions from '../../../test/fixtures/permissions.js';
import getApp from '../../app.js';
import { createServices } from '../../services/index.js';
import { vi } from 'vitest';
import { afterEach, vi } from 'vitest';
async function getSetup() {
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 () => {
const { request, base } = await getSetup();
return request
@ -71,6 +76,20 @@ test('create a new strategy with empty parameters', async () => {
.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 () => {
const { request, base, strategyStore } = await getSetup();
strategyStore.createStrategy({ name: 'Testing', parameters: [] });
@ -92,6 +111,22 @@ test('update strategy', async () => {
.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 () => {
const { request, base } = await getSetup();
const name = 'UnknownStrat';

View File

@ -17,7 +17,7 @@ import {
StrategyUpdatedEvent,
} from '../types/index.js';
import strategySchema from './strategy-schema.js';
import { NameExistsError } from '../error/index.js';
import { NameExistsError, OperationDeniedError } from '../error/index.js';
class StrategyService {
private logger: Logger;
@ -26,14 +26,20 @@ class StrategyService {
private eventService: EventService;
private customStrategySettings: IUnleashConfig['customStrategySettings'];
constructor(
{ strategyStore }: Pick<IUnleashStores, 'strategyStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
{
getLogger,
customStrategySettings,
}: Pick<IUnleashConfig, 'getLogger' | 'customStrategySettings'>,
eventService: EventService,
) {
this.strategyStore = strategyStore;
this.eventService = eventService;
this.logger = getLogger('services/strategy-service.js');
this.customStrategySettings = customStrategySettings;
}
async getStrategies(): Promise<IStrategy[]> {
@ -103,6 +109,7 @@ class StrategyService {
value: IMinimalStrategy,
auditUser: IAuditUser,
): Promise<IStrategy | undefined> {
this.assertCreationAllowed();
const strategy = await strategySchema.validateAsync(value);
strategy.deprecated = false;
await this._validateStrategyName(strategy);
@ -120,6 +127,7 @@ class StrategyService {
input: IMinimalStrategy,
auditUser: IAuditUser,
): Promise<void> {
this.assertEditingAllowed();
const value = await strategySchema.validateAsync(input);
const strategy = await this.strategyStore.get(input.name);
await this._validateEditable(strategy);
@ -155,5 +163,21 @@ class StrategyService {
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;

View File

@ -118,6 +118,11 @@ export interface IClientCachingOption {
maxAge: number;
}
export interface ICustomStrategySettings {
disableCreation: boolean;
disableEditing: boolean;
}
export interface ResourceLimits {
apiTokens: number;
constraints: number;
@ -175,6 +180,7 @@ export interface IUnleashOptions {
resourceLimits?: Partial<ResourceLimits>;
userInactivityThresholdInDays?: number;
unleashFrontendToken?: string;
customStrategySettings?: ICustomStrategySettings;
checkDbOnReady?: boolean;
}
@ -302,5 +308,6 @@ export interface IUnleashConfig {
userInactivityThresholdInDays: number;
buildDate?: string;
unleashFrontendToken?: string;
customStrategySettings?: ICustomStrategySettings;
checkDbOnReady?: boolean;
}