From a7efa2f097288c0dcdc5d4e8783817dbf97d85f4 Mon Sep 17 00:00:00 2001 From: Andreas Fitzek Date: Tue, 9 Dec 2025 14:27:48 +0100 Subject: [PATCH] chore(core): License flag for the dynamic credential module (#22971) --- packages/@n8n/backend-common/src/license-state.ts | 4 ++++ packages/@n8n/constants/src/index.ts | 1 + packages/cli/src/controllers/e2e.controller.ts | 1 + packages/cli/src/license.ts | 5 +++++ .../dynamic-credentials.module.ts | 14 +++++++++++++- .../credential-resolvers.api.test.ts | 6 ++++-- .../dynamic-credential-entry-storage.test.ts | 6 +++++- .../dynamic-credential-entry.repository.test.ts | 8 ++++++-- 8 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/@n8n/backend-common/src/license-state.ts b/packages/@n8n/backend-common/src/license-state.ts index 8fcd3a9e64c..9a14dec874d 100644 --- a/packages/@n8n/backend-common/src/license-state.ts +++ b/packages/@n8n/backend-common/src/license-state.ts @@ -58,6 +58,10 @@ export class LicenseState { return this.isLicensed(LICENSE_FEATURES.CUSTOM_ROLES); } + isDynamicCredentialsLicensed() { + return this.isLicensed(LICENSE_FEATURES.DYNAMIC_CREDENTIALS); + } + isSharingLicensed() { return this.isLicensed('feat:sharing'); } diff --git a/packages/@n8n/constants/src/index.ts b/packages/@n8n/constants/src/index.ts index 286c0ecb3ec..264fcf2a2b2 100644 --- a/packages/@n8n/constants/src/index.ts +++ b/packages/@n8n/constants/src/index.ts @@ -38,6 +38,7 @@ export const LICENSE_FEATURES = { WORKFLOW_DIFFS: 'feat:workflowDiffs', CUSTOM_ROLES: 'feat:customRoles', AI_BUILDER: 'feat:aiBuilder', + DYNAMIC_CREDENTIALS: 'feat:dynamicCredentials', } as const; export const LICENSE_QUOTAS = { diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 8faf4910330..4d57777969b 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -86,6 +86,7 @@ type PushRequest = Request< @RestController('/e2e') export class E2EController { private enabledFeatures: Record = { + [LICENSE_FEATURES.DYNAMIC_CREDENTIALS]: false, [LICENSE_FEATURES.SHARING]: false, [LICENSE_FEATURES.LDAP]: false, [LICENSE_FEATURES.SAML]: false, diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index db7280f0b6c..d6cbc91aaff 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -221,6 +221,11 @@ export class License implements LicenseProvider { return this.manager?.hasFeatureEnabled(feature) ?? false; } + /** @deprecated Use `LicenseState.isDynamicCredentialsLicensed` instead. */ + isDynamicCredentialsEnabled() { + return this.isLicensed(LICENSE_FEATURES.DYNAMIC_CREDENTIALS); + } + /** @deprecated Use `LicenseState.isSharingLicensed` instead. */ isSharingEnabled() { return this.isLicensed(LICENSE_FEATURES.SHARING); diff --git a/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts b/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts index 4155bf7381e..d1f44323057 100644 --- a/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts +++ b/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts @@ -1,10 +1,19 @@ +import { LICENSE_FEATURES } from '@n8n/constants'; import type { ModuleInterface } from '@n8n/decorators'; import { BackendModule, OnShutdown } from '@n8n/decorators'; import { Container } from '@n8n/di'; -@BackendModule({ name: 'dynamic-credentials', licenseFlag: 'feat:externalSecrets' }) +function isFeatureFlagEnabled(): boolean { + return process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS === 'true'; +} + +// TODO: Remove LICENSE_FEATURES.EXTERNAL_SECRETS with dynamic credentials feature once it is in the license server +@BackendModule({ name: 'dynamic-credentials', licenseFlag: LICENSE_FEATURES.EXTERNAL_SECRETS }) export class DynamicCredentialsModule implements ModuleInterface { async init() { + if (!isFeatureFlagEnabled()) { + return; + } await import('./dynamic-credentials.controller'); await import('./credential-resolvers.controller'); await import('./context-establishment-hooks'); @@ -15,6 +24,9 @@ export class DynamicCredentialsModule implements ModuleInterface { } async entities() { + if (!isFeatureFlagEnabled()) { + return []; + } const { DynamicCredentialResolver } = await import('./database/entities/credential-resolver'); const { DynamicCredentialEntry } = await import('./database/entities/dynamic-credential-entry'); diff --git a/packages/cli/test/integration/dynamic-credentials.ee/credential-resolvers.api.test.ts b/packages/cli/test/integration/dynamic-credentials.ee/credential-resolvers.api.test.ts index af0368d5d1f..dbc47214cd5 100644 --- a/packages/cli/test/integration/dynamic-credentials.ee/credential-resolvers.api.test.ts +++ b/packages/cli/test/integration/dynamic-credentials.ee/credential-resolvers.api.test.ts @@ -5,9 +5,9 @@ import { GLOBAL_OWNER_ROLE, GLOBAL_MEMBER_ROLE } from '@n8n/db'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; -import { Telemetry } from '@/telemetry'; -import { DynamicCredentialResolverService } from '@/modules/dynamic-credentials.ee/services/credential-resolver.service'; import { DynamicCredentialResolverRepository } from '@/modules/dynamic-credentials.ee/database/repositories/credential-resolver.repository'; +import { DynamicCredentialResolverService } from '@/modules/dynamic-credentials.ee/services/credential-resolver.service'; +import { Telemetry } from '@/telemetry'; import { createUser } from '../shared/db/users'; import type { SuperAgentTest } from '../shared/types'; @@ -19,6 +19,8 @@ const licenseMock = mock(); licenseMock.isLicensed.mockReturnValue(true); Container.set(LicenseState, licenseMock); +process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS = 'true'; + const testServer = utils.setupTestServer({ endpointGroups: ['credentials'], enabledFeatures: ['feat:externalSecrets'], diff --git a/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry-storage.test.ts b/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry-storage.test.ts index 3c44ceae1a8..0f888d53d3e 100644 --- a/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry-storage.test.ts +++ b/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry-storage.test.ts @@ -3,19 +3,23 @@ import { Container } from '@n8n/di'; import { DynamicCredentialEntryStorage } from '@/modules/dynamic-credentials.ee/credential-resolvers/storage/dynamic-credential-entry-storage'; -import { createCredentials } from '../shared/db/credentials'; import { createDynamicCredentialResolver } from './shared/db-helpers'; +import { createCredentials } from '../shared/db/credentials'; describe('DynamicCredentialEntryStorage', () => { let storage: DynamicCredentialEntryStorage; + let previousEnvVar: string | undefined; beforeAll(async () => { + previousEnvVar = process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS; + process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS = 'true'; await testModules.loadModules(['dynamic-credentials']); await testDb.init(); storage = Container.get(DynamicCredentialEntryStorage); }); afterAll(async () => { + process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS = previousEnvVar; await testDb.terminate(); }); diff --git a/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry.repository.test.ts b/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry.repository.test.ts index 50384dd2990..b24d697510f 100644 --- a/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry.repository.test.ts +++ b/packages/cli/test/integration/dynamic-credentials/dynamic-credential-entry.repository.test.ts @@ -2,23 +2,27 @@ import { testDb, testModules } from '@n8n/backend-test-utils'; import { CredentialsRepository } from '@n8n/db'; import { Container } from '@n8n/di'; -import { DynamicCredentialEntryRepository } from '@/modules/dynamic-credentials.ee/database/repositories/dynamic-credential-entry.repository'; import { DynamicCredentialEntry } from '@/modules/dynamic-credentials.ee/database/entities/dynamic-credential-entry'; import { DynamicCredentialResolverRepository } from '@/modules/dynamic-credentials.ee/database/repositories/credential-resolver.repository'; +import { DynamicCredentialEntryRepository } from '@/modules/dynamic-credentials.ee/database/repositories/dynamic-credential-entry.repository'; -import { createCredentials } from '../shared/db/credentials'; import { createDynamicCredentialResolver } from './shared/db-helpers'; +import { createCredentials } from '../shared/db/credentials'; describe('DynamicCredentialEntryRepository', () => { let repository: DynamicCredentialEntryRepository; + let previousEnvVar: string | undefined; beforeAll(async () => { + previousEnvVar = process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS; + process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS = 'true'; await testModules.loadModules(['dynamic-credentials']); await testDb.init(); repository = Container.get(DynamicCredentialEntryRepository); }); afterAll(async () => { + process.env.N8N_ENV_FEAT_CONTEXT_ESTABLISHMENT_HOOKS = previousEnvVar; await testDb.terminate(); });