diff --git a/packages/cli/package.json b/packages/cli/package.json index 73d107672b..fd6045ed75 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.9.1", + "@n8n_io/license-sdk": "~1.8.0", "@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 c70159e4e0..3b7b369b80 100644 --- a/packages/cli/src/Ldap/constants.ts +++ b/packages/cli/src/Ldap/constants.ts @@ -2,6 +2,8 @@ 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 0785f7fdd1..6ed83242e0 100644 --- a/packages/cli/src/Ldap/helpers.ts +++ b/packages/cli/src/Ldap/helpers.ts @@ -17,6 +17,7 @@ import { LdapManager } from './LdapManager.ee'; import { BINARY_AD_ATTRIBUTES, LDAP_CONFIG_SCHEMA, + LDAP_ENABLED, LDAP_FEATURE_NAME, LDAP_LOGIN_ENABLED, LDAP_LOGIN_LABEL, @@ -36,7 +37,7 @@ import { */ export const isLdapEnabled = (): boolean => { const license = Container.get(License); - return isUserManagementEnabled() && license.isLdapEnabled(); + return isUserManagementEnabled() && (config.getEnv(LDAP_ENABLED) || license.isLdapEnabled()); }; /** diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index de9d30e29e..9dc68d86a5 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -8,11 +8,6 @@ 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, @@ -23,8 +18,6 @@ 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 1609b3ba28..5bffa178c7 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: false, - advancedExecutionFilters: false, + logStreaming: config.getEnv('enterprise.features.logStreaming'), + advancedExecutionFilters: config.getEnv('enterprise.features.advancedExecutionFilters'), }, hideUsagePage: config.getEnv('hideUsagePage'), license: { diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 2e4682db2e..4c34a57b31 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -57,7 +57,10 @@ export function isUserManagementEnabled(): boolean { export function isSharingEnabled(): boolean { const license = Container.get(License); - return isUserManagementEnabled() && license.isSharingEnabled(); + return ( + isUserManagementEnabled() && + (config.getEnv('enterprise.features.sharing') || 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 fb9097da61..befd913114 100644 --- a/packages/cli/src/api/e2e.api.ts +++ b/packages/cli/src/api/e2e.api.ts @@ -5,7 +5,6 @@ /* 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'; @@ -13,26 +12,12 @@ 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', @@ -93,7 +78,7 @@ const setupUserManagement = async () => { }; const resetLogStreaming = async () => { - enabledFeatures.logStreaming = false; + config.set('enterprise.features.logStreaming', false); for (const id in eventBus.destinations) { await eventBus.removeDestination(id); } @@ -142,8 +127,7 @@ e2eController.post('/db/setup-owner', bodyParser.json(), async (req, res) => { res.writeHead(204).end(); }); -e2eController.post('/enable-feature/:feature', async (req: Request<{ feature: Feature }>, res) => { - const { feature } = req.params; - enabledFeatures[feature] = true; +e2eController.post('/enable-feature/:feature', async (req, res) => { + config.set(`enterprise.features.${req.params.feature}`, true); res.writeHead(204).end(); }); diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 4b87524611..f84c73cddb 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -26,6 +26,10 @@ 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 74f7ab64fc..5d9cb40045 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -990,6 +990,31 @@ 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, @@ -1141,12 +1166,6 @@ 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 29eab2872a..7dfc68e564 100644 --- a/packages/cli/src/eventbus/MessageEventBus/MessageEventBusHelper.ts +++ b/packages/cli/src/eventbus/MessageEventBus/MessageEventBusHelper.ts @@ -1,7 +1,8 @@ +import config from '@/config'; import { License } from '@/License'; import { Container } from 'typedi'; export function isLogStreamingEnabled(): boolean { const license = Container.get(License); - return license.isLogStreamingEnabled(); + return config.getEnv('enterprise.features.logStreaming') || license.isLogStreamingEnabled(); } diff --git a/packages/cli/src/executions/executionHelpers.ts b/packages/cli/src/executions/executionHelpers.ts index 148bd9b8b9..de27577e22 100644 --- a/packages/cli/src/executions/executionHelpers.ts +++ b/packages/cli/src/executions/executionHelpers.ts @@ -2,6 +2,7 @@ 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, @@ -21,5 +22,8 @@ export function getStatusUsingPreviousExecutionStatusMethod( export function isAdvancedExecutionFiltersEnabled(): boolean { const license = Container.get(License); - return license.isAdvancedExecutionFiltersEnabled(); + return ( + config.getEnv('enterprise.features.advancedExecutionFilters') || + license.isAdvancedExecutionFiltersEnabled() + ); } diff --git a/packages/cli/src/sso/saml/constants.ts b/packages/cli/src/sso/saml/constants.ts index 3729f3ce51..6f92690f5f 100644 --- a/packages/cli/src/sso/saml/constants.ts +++ b/packages/cli/src/sso/saml/constants.ts @@ -28,6 +28,8 @@ 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 5975c02653..57c710e707 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_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants'; +import { SAML_ENTERPRISE_FEATURE_ENABLED, SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants'; import { isEmailCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod, @@ -52,7 +52,10 @@ export function setSamlLoginLabel(label: string): void { export function isSamlLicensed(): boolean { const license = Container.get(License); - return isUserManagementEnabled() && license.isSamlEnabled(); + return ( + isUserManagementEnabled() && + (license.isSamlEnabled() || config.getEnv(SAML_ENTERPRISE_FEATURE_ENABLED)) + ); } export function isSamlLicensedAndEnabled(): boolean { diff --git a/packages/cli/test/integration/eventbus.test.ts b/packages/cli/test/integration/eventbus.test.ts index f2cee9561b..d456f86799 100644 --- a/packages/cli/test/integration/eventbus.test.ts +++ b/packages/cli/test/integration/eventbus.test.ts @@ -23,8 +23,6 @@ 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'); @@ -79,7 +77,6 @@ async function confirmIdSent(id: string) { } beforeAll(async () => { - Container.get(License).isLogStreamingEnabled = () => true; app = await utils.initTestServer({ endpointGroups: ['eventBus'] }); globalOwnerRole = await testDb.getGlobalOwnerRole(); @@ -104,6 +101,7 @@ 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); @@ -112,7 +110,6 @@ beforeAll(async () => { afterAll(async () => { jest.mock('@/eventbus/MessageEventBus/MessageEventBus'); - Container.reset(); await testDb.terminate(); await eventBus.close(); }); @@ -181,6 +178,7 @@ 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! @@ -221,6 +219,7 @@ 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! @@ -256,6 +255,7 @@ test('should anonymize audit message to syslog ', async () => { }, id: uuid(), }); + config.set('enterprise.features.logStreaming', true); const syslogDestination = eventBus.destinations[ testSyslogDestination.id! @@ -317,6 +317,7 @@ 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! @@ -351,6 +352,7 @@ 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 61a44b59ea..fcecb25d8f 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -17,8 +17,6 @@ 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'); @@ -43,7 +41,6 @@ const defaultLdapConfig = { }; beforeAll(async () => { - Container.get(License).isLdapEnabled = () => true; app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'] }); const [globalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles(); @@ -80,10 +77,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 72410c56be..a3abb177aa 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -7,25 +7,22 @@ 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 351d6bf3b4..e8bf676ba7 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': - Container.get(License).isLdapEnabled = () => true; + config.set(LDAP_ENABLED, 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 11eb0f29d2..c544416eee 100644 --- a/packages/cli/test/integration/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows.controller.ee.test.ts @@ -12,8 +12,6 @@ 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; @@ -25,7 +23,6 @@ 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(); @@ -45,6 +42,8 @@ beforeAll(async () => { sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); await utils.initNodeTypes(); + + config.set('enterprise.features.sharing', true); }); beforeEach(async () => { @@ -52,7 +51,6 @@ beforeEach(async () => { }); afterAll(async () => { - Container.reset(); await testDb.terminate(); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23750a4cf2..3a7be8281e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,7 +113,7 @@ importers: packages/cli: specifiers: '@apidevtools/swagger-cli': 4.0.0 - '@n8n_io/license-sdk': ^1.9.1 + '@n8n_io/license-sdk': ~1.8.0 '@oclif/command': ^1.8.16 '@oclif/core': ^1.16.4 '@oclif/dev-cli': ^1.22.2 @@ -256,7 +256,7 @@ importers: xmllint-wasm: ^3.0.1 yamljs: ^0.3.0 dependencies: - '@n8n_io/license-sdk': 1.9.1 + '@n8n_io/license-sdk': 1.8.0 '@oclif/command': 1.8.18_@oclif+config@1.18.5 '@oclif/core': 1.16.6 '@oclif/errors': 1.3.6 @@ -3744,14 +3744,16 @@ packages: dev: false optional: true - /@n8n_io/license-sdk/1.9.1: - resolution: {integrity: sha512-M7tvmYpPSqVDQcxQUKW6XB26zXBZXRTocHtLvE+QJQ4mMbGg2KGwJsYwO9W4DGe8lQ7YD8ZMzG0fLzt0It+zGQ==} + /@n8n_io/license-sdk/1.8.0: + resolution: {integrity: sha512-dSBD6EHTu6kWWz1ILxtCcaQqVZu+p/8J0eQ2ntx7Jk8BYSvn5Hh4Oz5M81ut9Pz+2uak+GnIuI6KeYUe1QBXIQ==} engines: {node: '>=14.0.0', npm: '>=7.10.0'} dependencies: + axios: 1.1.3 crypto-js: 4.1.1 node-machine-id: 1.1.12 node-rsa: 1.1.1 - undici: 5.21.0 + transitivePeerDependencies: + - debug dev: false /@n8n_io/riot-tmpl/3.0.0: @@ -7758,6 +7760,16 @@ packages: - debug dev: false + /axios/1.1.3: + resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /babel-core/7.0.0-bridge.0_@babel+core@7.20.12: resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -20397,13 +20409,6 @@ packages: undertaker-registry: 1.0.1 dev: true - /undici/5.21.0: - resolution: {integrity: sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==} - engines: {node: '>=12.18'} - dependencies: - busboy: 1.6.0 - dev: false - /unescape/1.0.1: resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} engines: {node: '>=0.10.0'}