mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
feat(core): Add feature flag for custom instance roles
This commit is contained in:
@@ -213,6 +213,9 @@ export interface FrontendSettings {
|
||||
folders: {
|
||||
enabled: boolean;
|
||||
};
|
||||
customInstanceRoles: {
|
||||
enabled: boolean;
|
||||
};
|
||||
banners: {
|
||||
dismissed: string[];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { GlobalConfig } from '../../index';
|
||||
|
||||
describe('RolesConfig', () => {
|
||||
beforeEach(() => {
|
||||
Container.reset();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should default the custom instance roles flag to off', () => {
|
||||
const { roles } = Container.get(GlobalConfig);
|
||||
|
||||
expect(roles.customInstanceRolesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should enable the custom instance roles flag from env', () => {
|
||||
vi.stubEnv('N8N_CUSTOM_INSTANCE_ROLES_ENABLED', 'true');
|
||||
|
||||
const { roles } = Container.get(GlobalConfig);
|
||||
|
||||
expect(roles.customInstanceRolesEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Config, Env } from '../decorators';
|
||||
|
||||
@Config
|
||||
export class RolesConfig {
|
||||
/**
|
||||
* Dark-launch flag for the custom instance roles UI. When off, the
|
||||
* instance-roles surface is not exposed in the frontend.
|
||||
*/
|
||||
@Env('N8N_CUSTOM_INSTANCE_ROLES_ENABLED')
|
||||
customInstanceRolesEnabled: boolean = false;
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import { NodesConfig } from './configs/nodes.config';
|
||||
import { PersonalizationConfig } from './configs/personalization.config';
|
||||
import { PublicApiConfig } from './configs/public-api.config';
|
||||
import { RedisConfig } from './configs/redis.config';
|
||||
import { RolesConfig } from './configs/roles.config';
|
||||
import { TaskRunnersConfig } from './configs/runners.config';
|
||||
import { ScalingModeConfig } from './configs/scaling-mode.config';
|
||||
import { SecurityConfig } from './configs/security.config';
|
||||
@@ -84,6 +85,7 @@ export { PasswordConfig } from './configs/password.config';
|
||||
export { AgentsConfig } from './configs/agents.config';
|
||||
export { CompressionNodeConfig } from './configs/compression.config';
|
||||
export { RedisConfig } from './configs/redis.config';
|
||||
export { RolesConfig } from './configs/roles.config';
|
||||
export { EndpointsConfig, PrometheusMetricsConfig };
|
||||
|
||||
const protocolSchema = z.enum(['http', 'https']);
|
||||
@@ -284,4 +286,7 @@ export class GlobalConfig {
|
||||
|
||||
@Nested
|
||||
instanceSettingsLoader: InstanceSettingsLoaderConfig;
|
||||
|
||||
@Nested
|
||||
roles: RolesConfig;
|
||||
}
|
||||
|
||||
@@ -597,6 +597,9 @@ describe('GlobalConfig', () => {
|
||||
daytonaApiUrl: '',
|
||||
daytonaApiKey: '',
|
||||
},
|
||||
roles: {
|
||||
customInstanceRolesEnabled: false,
|
||||
},
|
||||
} satisfies GlobalConfigShape;
|
||||
|
||||
it('should use all default values when no env variables are defined', () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('FrontendService', () => {
|
||||
tags: { disabled: false },
|
||||
logging: { level: 'info' },
|
||||
hiringBanner: { enabled: false },
|
||||
roles: { customInstanceRolesEnabled: false },
|
||||
versionNotifications: {
|
||||
enabled: false,
|
||||
endpoint: '',
|
||||
|
||||
@@ -399,6 +399,9 @@ export class FrontendService {
|
||||
folders: {
|
||||
enabled: false,
|
||||
},
|
||||
customInstanceRoles: {
|
||||
enabled: this.globalConfig.roles.customInstanceRolesEnabled,
|
||||
},
|
||||
evaluation: {
|
||||
quota: this.licenseState.getMaxWorkflowsWithEvaluations(),
|
||||
},
|
||||
|
||||
@@ -176,6 +176,9 @@ export const defaultSettings: FrontendSettings = {
|
||||
folders: {
|
||||
enabled: false,
|
||||
},
|
||||
customInstanceRoles: {
|
||||
enabled: false,
|
||||
},
|
||||
evaluation: {
|
||||
quota: 0,
|
||||
},
|
||||
|
||||
@@ -312,4 +312,56 @@ describe('settings.store', () => {
|
||||
expect(settingsStore.isOtelCustomSpanAttributesEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomInstanceRolesEnabled', () => {
|
||||
it('should return true when the dark-launch flag is on', async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...mockSettings,
|
||||
customInstanceRoles: { enabled: true },
|
||||
});
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
await settingsStore.getSettings();
|
||||
|
||||
expect(settingsStore.isCustomInstanceRolesEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when the dark-launch flag is off', async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...mockSettings,
|
||||
customInstanceRoles: { enabled: false },
|
||||
});
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
await settingsStore.getSettings();
|
||||
|
||||
expect(settingsStore.isCustomInstanceRolesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when the setting is absent', async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...mockSettings,
|
||||
customInstanceRoles: undefined,
|
||||
});
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
await settingsStore.getSettings();
|
||||
|
||||
expect(settingsStore.isCustomInstanceRolesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should stay off when the customRoles license is on but the flag is off (gates are independent)', async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...mockSettings,
|
||||
enterprise: { customRoles: true },
|
||||
customInstanceRoles: { enabled: false },
|
||||
});
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
await settingsStore.getSettings();
|
||||
|
||||
expect(settingsStore.isCustomRolesFeatureEnabled).toBe(true);
|
||||
expect(settingsStore.isCustomInstanceRolesEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,6 +201,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
() => settings.value.enterprise?.customRoles ?? false,
|
||||
);
|
||||
|
||||
const isCustomInstanceRolesEnabled = computed(
|
||||
() => settings.value.customInstanceRoles?.enabled ?? false,
|
||||
);
|
||||
|
||||
const areTagsEnabled = computed(() =>
|
||||
settings.value.workflowTagsDisabled !== undefined ? !settings.value.workflowTagsDisabled : true,
|
||||
);
|
||||
@@ -440,6 +444,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||
isFoldersFeatureEnabled,
|
||||
isAiAssistantEnabled,
|
||||
isCustomRolesFeatureEnabled,
|
||||
isCustomInstanceRolesEnabled,
|
||||
areTagsEnabled,
|
||||
isAutosaveEnabled,
|
||||
isHiringBannerEnabled,
|
||||
|
||||
Reference in New Issue
Block a user