chore(core): License flag for the dynamic credential module (#22971)

This commit is contained in:
Andreas Fitzek
2025-12-09 14:27:48 +01:00
committed by GitHub
parent 51e4adf80b
commit a7efa2f097
8 changed files with 39 additions and 6 deletions
@@ -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');
}
+1
View File
@@ -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 = {
@@ -86,6 +86,7 @@ type PushRequest = Request<
@RestController('/e2e')
export class E2EController {
private enabledFeatures: Record<BooleanLicenseFeature, boolean> = {
[LICENSE_FEATURES.DYNAMIC_CREDENTIALS]: false,
[LICENSE_FEATURES.SHARING]: false,
[LICENSE_FEATURES.LDAP]: false,
[LICENSE_FEATURES.SAML]: false,
+5
View File
@@ -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);
@@ -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');
@@ -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<LicenseState>();
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'],
@@ -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();
});
@@ -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();
});