diff --git a/packages/cli/package.json b/packages/cli/package.json index c32045589e..73d107672b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -114,7 +114,7 @@ "tsconfig-paths": "^4.1.2" }, "dependencies": { - "@n8n_io/license-sdk": "^1.8.0", + "@n8n_io/license-sdk": "^1.9.1", "@oclif/command": "^1.8.16", "@oclif/core": "^1.16.4", "@oclif/errors": "^1.3.6", diff --git a/packages/cli/src/Ldap/constants.ts b/packages/cli/src/Ldap/constants.ts index 3b7b369b80..c70159e4e0 100644 --- a/packages/cli/src/Ldap/constants.ts +++ b/packages/cli/src/Ldap/constants.ts @@ -2,8 +2,6 @@ import type { LdapConfig } from './types'; export const LDAP_FEATURE_NAME = 'features.ldap'; -export const LDAP_ENABLED = 'enterprise.features.ldap'; - export const LDAP_LOGIN_LABEL = 'sso.ldap.loginLabel'; export const LDAP_LOGIN_ENABLED = 'sso.ldap.loginEnabled'; diff --git a/packages/cli/src/Ldap/helpers.ts b/packages/cli/src/Ldap/helpers.ts index 6ed83242e0..0785f7fdd1 100644 --- a/packages/cli/src/Ldap/helpers.ts +++ b/packages/cli/src/Ldap/helpers.ts @@ -17,7 +17,6 @@ import { LdapManager } from './LdapManager.ee'; import { BINARY_AD_ATTRIBUTES, LDAP_CONFIG_SCHEMA, - LDAP_ENABLED, LDAP_FEATURE_NAME, LDAP_LOGIN_ENABLED, LDAP_LOGIN_LABEL, @@ -37,7 +36,7 @@ import { */ export const isLdapEnabled = (): boolean => { const license = Container.get(License); - return isUserManagementEnabled() && (config.getEnv(LDAP_ENABLED) || license.isLdapEnabled()); + return isUserManagementEnabled() && license.isLdapEnabled(); }; /** diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index 9dc68d86a5..de9d30e29e 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -8,6 +8,11 @@ import { LICENSE_FEATURES, N8N_VERSION, SETTINGS_LICENSE_CERT_KEY } from './cons import { Service } from 'typedi'; async function loadCertStr(): Promise { + // if we have an ephemeral license, we don't want to load it from the database + const ephemeralLicense = config.get('license.cert'); + if (ephemeralLicense) { + return ephemeralLicense; + } const databaseSettings = await Db.collections.Settings.findOne({ where: { key: SETTINGS_LICENSE_CERT_KEY, @@ -18,6 +23,8 @@ async function loadCertStr(): Promise { } async function saveCertStr(value: TLicenseContainerStr): Promise { + // if we have an ephemeral license, we don't want to save it to the database + if (config.get('license.cert')) return; await Db.collections.Settings.upsert( { key: SETTINGS_LICENSE_CERT_KEY, diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 5bffa178c7..1609b3ba28 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -311,8 +311,8 @@ class Server extends AbstractServer { sharing: false, ldap: false, saml: false, - logStreaming: config.getEnv('enterprise.features.logStreaming'), - advancedExecutionFilters: config.getEnv('enterprise.features.advancedExecutionFilters'), + logStreaming: false, + advancedExecutionFilters: false, }, hideUsagePage: config.getEnv('hideUsagePage'), license: { diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 4c34a57b31..2e4682db2e 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -57,10 +57,7 @@ export function isUserManagementEnabled(): boolean { export function isSharingEnabled(): boolean { const license = Container.get(License); - return ( - isUserManagementEnabled() && - (config.getEnv('enterprise.features.sharing') || license.isSharingEnabled()) - ); + return isUserManagementEnabled() && license.isSharingEnabled(); } export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise { diff --git a/packages/cli/src/api/e2e.api.ts b/packages/cli/src/api/e2e.api.ts index befd913114..fb9097da61 100644 --- a/packages/cli/src/api/e2e.api.ts +++ b/packages/cli/src/api/e2e.api.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/naming-convention */ import { Router } from 'express'; +import type { Request } from 'express'; import bodyParser from 'body-parser'; import { v4 as uuid } from 'uuid'; import config from '@/config'; @@ -12,12 +13,26 @@ import * as Db from '@/Db'; import type { Role } from '@db/entities/Role'; import { hashPassword } from '@/UserManagement/UserManagementHelper'; import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; +import Container from 'typedi'; +import { License } from '../License'; if (process.env.E2E_TESTS !== 'true') { console.error('E2E endpoints only allowed during E2E tests'); process.exit(1); } +const enabledFeatures = { + sharing: true, //default to true here instead of setting it in config/index.ts for e2e + ldap: false, + saml: false, + logStreaming: false, + advancedExecutionFilters: false, +}; + +type Feature = keyof typeof enabledFeatures; + +Container.get(License).isFeatureEnabled = (feature: Feature) => enabledFeatures[feature] ?? false; + const tablesToTruncate = [ 'auth_identity', 'auth_provider_sync_history', @@ -78,7 +93,7 @@ const setupUserManagement = async () => { }; const resetLogStreaming = async () => { - config.set('enterprise.features.logStreaming', false); + enabledFeatures.logStreaming = false; for (const id in eventBus.destinations) { await eventBus.removeDestination(id); } @@ -127,7 +142,8 @@ e2eController.post('/db/setup-owner', bodyParser.json(), async (req, res) => { res.writeHead(204).end(); }); -e2eController.post('/enable-feature/:feature', async (req, res) => { - config.set(`enterprise.features.${req.params.feature}`, true); +e2eController.post('/enable-feature/:feature', async (req: Request<{ feature: Feature }>, res) => { + const { feature } = req.params; + enabledFeatures[feature] = true; res.writeHead(204).end(); }); diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index f84c73cddb..4b87524611 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -26,10 +26,6 @@ if (inE2ETests) { const config = convict(schema, { args: [] }); -if (inE2ETests) { - config.set('enterprise.features.sharing', true); -} - // eslint-disable-next-line @typescript-eslint/unbound-method config.getEnv = config.get; diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 5d9cb40045..74f7ab64fc 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -990,31 +990,6 @@ export const schema = { }, }, - enterprise: { - features: { - sharing: { - format: Boolean, - default: false, - }, - ldap: { - format: Boolean, - default: false, - }, - saml: { - format: Boolean, - default: false, - }, - logStreaming: { - format: Boolean, - default: false, - }, - advancedExecutionFilters: { - format: Boolean, - default: false, - }, - }, - }, - sso: { justInTimeProvisioning: { format: Boolean, @@ -1166,6 +1141,12 @@ export const schema = { env: 'N8N_LICENSE_TENANT_ID', doc: 'Tenant id used by the license manager', }, + cert: { + format: String, + default: '', + env: 'N8N_LICENSE_CERT', + doc: 'Ephemeral license certificate', + }, }, hideUsagePage: { diff --git a/packages/cli/src/eventbus/MessageEventBus/MessageEventBusHelper.ts b/packages/cli/src/eventbus/MessageEventBus/MessageEventBusHelper.ts index 7dfc68e564..29eab2872a 100644 --- a/packages/cli/src/eventbus/MessageEventBus/MessageEventBusHelper.ts +++ b/packages/cli/src/eventbus/MessageEventBus/MessageEventBusHelper.ts @@ -1,8 +1,7 @@ -import config from '@/config'; import { License } from '@/License'; import { Container } from 'typedi'; export function isLogStreamingEnabled(): boolean { const license = Container.get(License); - return config.getEnv('enterprise.features.logStreaming') || license.isLogStreamingEnabled(); + return license.isLogStreamingEnabled(); } diff --git a/packages/cli/src/executions/executionHelpers.ts b/packages/cli/src/executions/executionHelpers.ts index de27577e22..148bd9b8b9 100644 --- a/packages/cli/src/executions/executionHelpers.ts +++ b/packages/cli/src/executions/executionHelpers.ts @@ -2,7 +2,6 @@ import { Container } from 'typedi'; import type { IExecutionFlattedDb } from '@/Interfaces'; import type { ExecutionStatus } from 'n8n-workflow'; import { License } from '@/License'; -import config from '@/config'; export function getStatusUsingPreviousExecutionStatusMethod( execution: IExecutionFlattedDb, @@ -22,8 +21,5 @@ export function getStatusUsingPreviousExecutionStatusMethod( export function isAdvancedExecutionFiltersEnabled(): boolean { const license = Container.get(License); - return ( - config.getEnv('enterprise.features.advancedExecutionFilters') || - license.isAdvancedExecutionFiltersEnabled() - ); + return license.isAdvancedExecutionFiltersEnabled(); } diff --git a/packages/cli/src/sso/saml/constants.ts b/packages/cli/src/sso/saml/constants.ts index 6f92690f5f..3729f3ce51 100644 --- a/packages/cli/src/sso/saml/constants.ts +++ b/packages/cli/src/sso/saml/constants.ts @@ -28,8 +28,6 @@ export class SamlUrls { export const SAML_PREFERENCES_DB_KEY = 'features.saml'; -export const SAML_ENTERPRISE_FEATURE_ENABLED = 'enterprise.features.saml'; - export const SAML_LOGIN_LABEL = 'sso.saml.loginLabel'; export const SAML_LOGIN_ENABLED = 'sso.saml.loginEnabled'; diff --git a/packages/cli/src/sso/saml/samlHelpers.ts b/packages/cli/src/sso/saml/samlHelpers.ts index 57c710e707..5975c02653 100644 --- a/packages/cli/src/sso/saml/samlHelpers.ts +++ b/packages/cli/src/sso/saml/samlHelpers.ts @@ -10,7 +10,7 @@ import type { SamlPreferences } from './types/samlPreferences'; import type { SamlUserAttributes } from './types/samlUserAttributes'; import type { FlowResult } from 'samlify/types/src/flow'; import type { SamlAttributeMapping } from './types/samlAttributeMapping'; -import { SAML_ENTERPRISE_FEATURE_ENABLED, SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants'; +import { SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants'; import { isEmailCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod, @@ -52,10 +52,7 @@ export function setSamlLoginLabel(label: string): void { export function isSamlLicensed(): boolean { const license = Container.get(License); - return ( - isUserManagementEnabled() && - (license.isSamlEnabled() || config.getEnv(SAML_ENTERPRISE_FEATURE_ENABLED)) - ); + return isUserManagementEnabled() && license.isSamlEnabled(); } export function isSamlLicensedAndEnabled(): boolean { diff --git a/packages/cli/test/integration/eventbus.test.ts b/packages/cli/test/integration/eventbus.test.ts index d456f86799..f2cee9561b 100644 --- a/packages/cli/test/integration/eventbus.test.ts +++ b/packages/cli/test/integration/eventbus.test.ts @@ -23,6 +23,8 @@ import { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDes import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit'; import { EventNamesTypes } from '@/eventbus/EventMessageClasses'; +import Container from 'typedi'; +import { License } from '../../src/License'; jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); jest.mock('axios'); @@ -77,6 +79,7 @@ async function confirmIdSent(id: string) { } beforeAll(async () => { + Container.get(License).isLogStreamingEnabled = () => true; app = await utils.initTestServer({ endpointGroups: ['eventBus'] }); globalOwnerRole = await testDb.getGlobalOwnerRole(); @@ -101,7 +104,6 @@ beforeAll(async () => { utils.initConfigFile(); config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter'); config.set('eventBus.logWriter.keepLogCount', 1); - config.set('enterprise.features.logStreaming', true); config.set('userManagement.disabled', false); config.set('userManagement.isInstanceOwnerSetUp', true); @@ -110,6 +112,7 @@ beforeAll(async () => { afterAll(async () => { jest.mock('@/eventbus/MessageEventBus/MessageEventBus'); + Container.reset(); await testDb.terminate(); await eventBus.close(); }); @@ -178,7 +181,6 @@ test.skip('should send message to syslog', async () => { eventName: 'n8n.test.message' as EventNamesTypes, id: uuid(), }); - config.set('enterprise.features.logStreaming', true); const syslogDestination = eventBus.destinations[ testSyslogDestination.id! @@ -219,7 +221,6 @@ test.skip('should confirm send message if there are no subscribers', async () => eventName: 'n8n.test.unsub' as EventNamesTypes, id: uuid(), }); - config.set('enterprise.features.logStreaming', true); const syslogDestination = eventBus.destinations[ testSyslogDestination.id! @@ -255,7 +256,6 @@ test('should anonymize audit message to syslog ', async () => { }, id: uuid(), }); - config.set('enterprise.features.logStreaming', true); const syslogDestination = eventBus.destinations[ testSyslogDestination.id! @@ -317,7 +317,6 @@ test('should send message to webhook ', async () => { eventName: 'n8n.test.message' as EventNamesTypes, id: uuid(), }); - config.set('enterprise.features.logStreaming', true); const webhookDestination = eventBus.destinations[ testWebhookDestination.id! @@ -352,7 +351,6 @@ test('should send message to sentry ', async () => { eventName: 'n8n.test.message' as EventNamesTypes, id: uuid(), }); - config.set('enterprise.features.logStreaming', true); const sentryDestination = eventBus.destinations[ testSentryDestination.id! diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index fcecb25d8f..61a44b59ea 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -17,6 +17,8 @@ import * as testDb from './../shared/testDb'; import type { AuthAgent } from '../shared/types'; import * as utils from '../shared/utils'; import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; +import Container from 'typedi'; +import { License } from '../../../src/License'; jest.mock('@/telemetry'); jest.mock('@/UserManagement/email/NodeMailer'); @@ -41,6 +43,7 @@ const defaultLdapConfig = { }; beforeAll(async () => { + Container.get(License).isLdapEnabled = () => true; app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'] }); const [globalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles(); @@ -77,10 +80,10 @@ beforeEach(async () => { config.set('userManagement.disabled', false); config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.emails.mode', ''); - config.set('enterprise.features.ldap', true); }); afterAll(async () => { + Container.reset(); await testDb.terminate(); }); diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index a3abb177aa..72410c56be 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -7,22 +7,25 @@ import { randomEmail, randomName, randomValidPassword } from '../shared/random'; import * as testDb from '../shared/testDb'; import * as utils from '../shared/utils'; import { sampleConfig } from './sampleMetadata'; +import Container from 'typedi'; +import { License } from '../../../src/License'; let owner: User; let authOwnerAgent: SuperAgentTest; async function enableSaml(enable: boolean) { await setSamlLoginEnabled(enable); - config.set('enterprise.features.saml', enable); } beforeAll(async () => { + Container.get(License).isSamlEnabled = () => true; const app = await utils.initTestServer({ endpointGroups: ['me', 'saml'] }); owner = await testDb.createOwner(); authOwnerAgent = utils.createAuthAgent(app)(owner); }); afterAll(async () => { + Container.reset(); await testDb.terminate(); }); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index e8bf676ba7..351d6bf3b4 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -74,13 +74,13 @@ import { InternalHooks } from '@/InternalHooks'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { PostHogClient } from '@/posthog'; import { LdapManager } from '@/Ldap/LdapManager.ee'; -import { LDAP_ENABLED } from '@/Ldap/constants'; import { handleLdapInit } from '@/Ldap/helpers'; import { Push } from '@/push'; import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; import { SamlService } from '@/sso/saml/saml.service.ee'; import { SamlController } from '@/sso/saml/routes/saml.controller.ee'; import { EventBusController } from '@/eventbus/eventBus.controller'; +import { License } from '../../../src/License'; export const mockInstance = ( ctor: new (...args: any[]) => T, @@ -186,7 +186,7 @@ export async function initTestServer({ ); break; case 'ldap': - config.set(LDAP_ENABLED, true); + Container.get(License).isLdapEnabled = () => true; await handleLdapInit(); const { service, sync } = LdapManager.getInstance(); registerController( diff --git a/packages/cli/test/integration/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows.controller.ee.test.ts index c544416eee..11eb0f29d2 100644 --- a/packages/cli/test/integration/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows.controller.ee.test.ts @@ -12,6 +12,8 @@ import { createWorkflow } from './shared/testDb'; import type { SaveCredentialFunction } from './shared/types'; import { makeWorkflow } from './shared/utils'; import { randomCredentialPayload } from './shared/random'; +import Container from 'typedi'; +import { License } from '../../src/License'; let owner: User; let member: User; @@ -23,6 +25,7 @@ let saveCredential: SaveCredentialFunction; let sharingSpy: jest.SpyInstance; beforeAll(async () => { + Container.get(License).isSharingEnabled = () => true; const app = await utils.initTestServer({ endpointGroups: ['workflows'] }); const globalOwnerRole = await testDb.getGlobalOwnerRole(); @@ -42,8 +45,6 @@ beforeAll(async () => { sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); await utils.initNodeTypes(); - - config.set('enterprise.features.sharing', true); }); beforeEach(async () => { @@ -51,6 +52,7 @@ beforeEach(async () => { }); afterAll(async () => { + Container.reset(); await testDb.terminate(); });