feat(core): Read ephemeral license from environment and clean up ee flags (#5797)

* remove enterprise feature schema for license.cert

* bump license sdk version

* Update packages/cli/package.json

Co-authored-by: Cornelius Suermann <cornelius@n8n.io>

---------

Co-authored-by: Cornelius Suermann <cornelius@n8n.io>
This commit is contained in:
Michael Auerswald 2023-03-28 17:21:40 +02:00 committed by GitHub
parent 5f6183a031
commit a81ca7c19c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 59 additions and 69 deletions

View file

@ -114,7 +114,7 @@
"tsconfig-paths": "^4.1.2" "tsconfig-paths": "^4.1.2"
}, },
"dependencies": { "dependencies": {
"@n8n_io/license-sdk": "^1.8.0", "@n8n_io/license-sdk": "^1.9.1",
"@oclif/command": "^1.8.16", "@oclif/command": "^1.8.16",
"@oclif/core": "^1.16.4", "@oclif/core": "^1.16.4",
"@oclif/errors": "^1.3.6", "@oclif/errors": "^1.3.6",

View file

@ -2,8 +2,6 @@ import type { LdapConfig } from './types';
export const LDAP_FEATURE_NAME = 'features.ldap'; 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_LABEL = 'sso.ldap.loginLabel';
export const LDAP_LOGIN_ENABLED = 'sso.ldap.loginEnabled'; export const LDAP_LOGIN_ENABLED = 'sso.ldap.loginEnabled';

View file

@ -17,7 +17,6 @@ import { LdapManager } from './LdapManager.ee';
import { import {
BINARY_AD_ATTRIBUTES, BINARY_AD_ATTRIBUTES,
LDAP_CONFIG_SCHEMA, LDAP_CONFIG_SCHEMA,
LDAP_ENABLED,
LDAP_FEATURE_NAME, LDAP_FEATURE_NAME,
LDAP_LOGIN_ENABLED, LDAP_LOGIN_ENABLED,
LDAP_LOGIN_LABEL, LDAP_LOGIN_LABEL,
@ -37,7 +36,7 @@ import {
*/ */
export const isLdapEnabled = (): boolean => { export const isLdapEnabled = (): boolean => {
const license = Container.get(License); const license = Container.get(License);
return isUserManagementEnabled() && (config.getEnv(LDAP_ENABLED) || license.isLdapEnabled()); return isUserManagementEnabled() && license.isLdapEnabled();
}; };
/** /**

View file

@ -8,6 +8,11 @@ import { LICENSE_FEATURES, N8N_VERSION, SETTINGS_LICENSE_CERT_KEY } from './cons
import { Service } from 'typedi'; import { Service } from 'typedi';
async function loadCertStr(): Promise<TLicenseContainerStr> { async function loadCertStr(): Promise<TLicenseContainerStr> {
// 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({ const databaseSettings = await Db.collections.Settings.findOne({
where: { where: {
key: SETTINGS_LICENSE_CERT_KEY, key: SETTINGS_LICENSE_CERT_KEY,
@ -18,6 +23,8 @@ async function loadCertStr(): Promise<TLicenseContainerStr> {
} }
async function saveCertStr(value: TLicenseContainerStr): Promise<void> { async function saveCertStr(value: TLicenseContainerStr): Promise<void> {
// 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( await Db.collections.Settings.upsert(
{ {
key: SETTINGS_LICENSE_CERT_KEY, key: SETTINGS_LICENSE_CERT_KEY,

View file

@ -311,8 +311,8 @@ class Server extends AbstractServer {
sharing: false, sharing: false,
ldap: false, ldap: false,
saml: false, saml: false,
logStreaming: config.getEnv('enterprise.features.logStreaming'), logStreaming: false,
advancedExecutionFilters: config.getEnv('enterprise.features.advancedExecutionFilters'), advancedExecutionFilters: false,
}, },
hideUsagePage: config.getEnv('hideUsagePage'), hideUsagePage: config.getEnv('hideUsagePage'),
license: { license: {

View file

@ -57,10 +57,7 @@ export function isUserManagementEnabled(): boolean {
export function isSharingEnabled(): boolean { export function isSharingEnabled(): boolean {
const license = Container.get(License); const license = Container.get(License);
return ( return isUserManagementEnabled() && license.isSharingEnabled();
isUserManagementEnabled() &&
(config.getEnv('enterprise.features.sharing') || license.isSharingEnabled())
);
} }
export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise<Role['id']> { export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise<Role['id']> {

View file

@ -5,6 +5,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { Router } from 'express'; import { Router } from 'express';
import type { Request } from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import config from '@/config'; import config from '@/config';
@ -12,12 +13,26 @@ import * as Db from '@/Db';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import { hashPassword } from '@/UserManagement/UserManagementHelper'; import { hashPassword } from '@/UserManagement/UserManagementHelper';
import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import Container from 'typedi';
import { License } from '../License';
if (process.env.E2E_TESTS !== 'true') { if (process.env.E2E_TESTS !== 'true') {
console.error('E2E endpoints only allowed during E2E tests'); console.error('E2E endpoints only allowed during E2E tests');
process.exit(1); 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 = [ const tablesToTruncate = [
'auth_identity', 'auth_identity',
'auth_provider_sync_history', 'auth_provider_sync_history',
@ -78,7 +93,7 @@ const setupUserManagement = async () => {
}; };
const resetLogStreaming = async () => { const resetLogStreaming = async () => {
config.set('enterprise.features.logStreaming', false); enabledFeatures.logStreaming = false;
for (const id in eventBus.destinations) { for (const id in eventBus.destinations) {
await eventBus.removeDestination(id); await eventBus.removeDestination(id);
} }
@ -127,7 +142,8 @@ e2eController.post('/db/setup-owner', bodyParser.json(), async (req, res) => {
res.writeHead(204).end(); res.writeHead(204).end();
}); });
e2eController.post('/enable-feature/:feature', async (req, res) => { e2eController.post('/enable-feature/:feature', async (req: Request<{ feature: Feature }>, res) => {
config.set(`enterprise.features.${req.params.feature}`, true); const { feature } = req.params;
enabledFeatures[feature] = true;
res.writeHead(204).end(); res.writeHead(204).end();
}); });

View file

@ -26,10 +26,6 @@ if (inE2ETests) {
const config = convict(schema, { args: [] }); const config = convict(schema, { args: [] });
if (inE2ETests) {
config.set('enterprise.features.sharing', true);
}
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
config.getEnv = config.get; config.getEnv = config.get;

View file

@ -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: { sso: {
justInTimeProvisioning: { justInTimeProvisioning: {
format: Boolean, format: Boolean,
@ -1166,6 +1141,12 @@ export const schema = {
env: 'N8N_LICENSE_TENANT_ID', env: 'N8N_LICENSE_TENANT_ID',
doc: 'Tenant id used by the license manager', doc: 'Tenant id used by the license manager',
}, },
cert: {
format: String,
default: '',
env: 'N8N_LICENSE_CERT',
doc: 'Ephemeral license certificate',
},
}, },
hideUsagePage: { hideUsagePage: {

View file

@ -1,8 +1,7 @@
import config from '@/config';
import { License } from '@/License'; import { License } from '@/License';
import { Container } from 'typedi'; import { Container } from 'typedi';
export function isLogStreamingEnabled(): boolean { export function isLogStreamingEnabled(): boolean {
const license = Container.get(License); const license = Container.get(License);
return config.getEnv('enterprise.features.logStreaming') || license.isLogStreamingEnabled(); return license.isLogStreamingEnabled();
} }

View file

@ -2,7 +2,6 @@ import { Container } from 'typedi';
import type { IExecutionFlattedDb } from '@/Interfaces'; import type { IExecutionFlattedDb } from '@/Interfaces';
import type { ExecutionStatus } from 'n8n-workflow'; import type { ExecutionStatus } from 'n8n-workflow';
import { License } from '@/License'; import { License } from '@/License';
import config from '@/config';
export function getStatusUsingPreviousExecutionStatusMethod( export function getStatusUsingPreviousExecutionStatusMethod(
execution: IExecutionFlattedDb, execution: IExecutionFlattedDb,
@ -22,8 +21,5 @@ export function getStatusUsingPreviousExecutionStatusMethod(
export function isAdvancedExecutionFiltersEnabled(): boolean { export function isAdvancedExecutionFiltersEnabled(): boolean {
const license = Container.get(License); const license = Container.get(License);
return ( return license.isAdvancedExecutionFiltersEnabled();
config.getEnv('enterprise.features.advancedExecutionFilters') ||
license.isAdvancedExecutionFiltersEnabled()
);
} }

View file

@ -28,8 +28,6 @@ export class SamlUrls {
export const SAML_PREFERENCES_DB_KEY = 'features.saml'; 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_LABEL = 'sso.saml.loginLabel';
export const SAML_LOGIN_ENABLED = 'sso.saml.loginEnabled'; export const SAML_LOGIN_ENABLED = 'sso.saml.loginEnabled';

View file

@ -10,7 +10,7 @@ import type { SamlPreferences } from './types/samlPreferences';
import type { SamlUserAttributes } from './types/samlUserAttributes'; import type { SamlUserAttributes } from './types/samlUserAttributes';
import type { FlowResult } from 'samlify/types/src/flow'; import type { FlowResult } from 'samlify/types/src/flow';
import type { SamlAttributeMapping } from './types/samlAttributeMapping'; 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 { import {
isEmailCurrentAuthenticationMethod, isEmailCurrentAuthenticationMethod,
isSamlCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod,
@ -52,10 +52,7 @@ export function setSamlLoginLabel(label: string): void {
export function isSamlLicensed(): boolean { export function isSamlLicensed(): boolean {
const license = Container.get(License); const license = Container.get(License);
return ( return isUserManagementEnabled() && license.isSamlEnabled();
isUserManagementEnabled() &&
(license.isSamlEnabled() || config.getEnv(SAML_ENTERPRISE_FEATURE_ENABLED))
);
} }
export function isSamlLicensedAndEnabled(): boolean { export function isSamlLicensedAndEnabled(): boolean {

View file

@ -23,6 +23,8 @@ import { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDes
import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee';
import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit'; import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit';
import { EventNamesTypes } from '@/eventbus/EventMessageClasses'; import { EventNamesTypes } from '@/eventbus/EventMessageClasses';
import Container from 'typedi';
import { License } from '../../src/License';
jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); jest.unmock('@/eventbus/MessageEventBus/MessageEventBus');
jest.mock('axios'); jest.mock('axios');
@ -77,6 +79,7 @@ async function confirmIdSent(id: string) {
} }
beforeAll(async () => { beforeAll(async () => {
Container.get(License).isLogStreamingEnabled = () => true;
app = await utils.initTestServer({ endpointGroups: ['eventBus'] }); app = await utils.initTestServer({ endpointGroups: ['eventBus'] });
globalOwnerRole = await testDb.getGlobalOwnerRole(); globalOwnerRole = await testDb.getGlobalOwnerRole();
@ -101,7 +104,6 @@ beforeAll(async () => {
utils.initConfigFile(); utils.initConfigFile();
config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter'); config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter');
config.set('eventBus.logWriter.keepLogCount', 1); config.set('eventBus.logWriter.keepLogCount', 1);
config.set('enterprise.features.logStreaming', true);
config.set('userManagement.disabled', false); config.set('userManagement.disabled', false);
config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
@ -110,6 +112,7 @@ beforeAll(async () => {
afterAll(async () => { afterAll(async () => {
jest.mock('@/eventbus/MessageEventBus/MessageEventBus'); jest.mock('@/eventbus/MessageEventBus/MessageEventBus');
Container.reset();
await testDb.terminate(); await testDb.terminate();
await eventBus.close(); await eventBus.close();
}); });
@ -178,7 +181,6 @@ test.skip('should send message to syslog', async () => {
eventName: 'n8n.test.message' as EventNamesTypes, eventName: 'n8n.test.message' as EventNamesTypes,
id: uuid(), id: uuid(),
}); });
config.set('enterprise.features.logStreaming', true);
const syslogDestination = eventBus.destinations[ const syslogDestination = eventBus.destinations[
testSyslogDestination.id! testSyslogDestination.id!
@ -219,7 +221,6 @@ test.skip('should confirm send message if there are no subscribers', async () =>
eventName: 'n8n.test.unsub' as EventNamesTypes, eventName: 'n8n.test.unsub' as EventNamesTypes,
id: uuid(), id: uuid(),
}); });
config.set('enterprise.features.logStreaming', true);
const syslogDestination = eventBus.destinations[ const syslogDestination = eventBus.destinations[
testSyslogDestination.id! testSyslogDestination.id!
@ -255,7 +256,6 @@ test('should anonymize audit message to syslog ', async () => {
}, },
id: uuid(), id: uuid(),
}); });
config.set('enterprise.features.logStreaming', true);
const syslogDestination = eventBus.destinations[ const syslogDestination = eventBus.destinations[
testSyslogDestination.id! testSyslogDestination.id!
@ -317,7 +317,6 @@ test('should send message to webhook ', async () => {
eventName: 'n8n.test.message' as EventNamesTypes, eventName: 'n8n.test.message' as EventNamesTypes,
id: uuid(), id: uuid(),
}); });
config.set('enterprise.features.logStreaming', true);
const webhookDestination = eventBus.destinations[ const webhookDestination = eventBus.destinations[
testWebhookDestination.id! testWebhookDestination.id!
@ -352,7 +351,6 @@ test('should send message to sentry ', async () => {
eventName: 'n8n.test.message' as EventNamesTypes, eventName: 'n8n.test.message' as EventNamesTypes,
id: uuid(), id: uuid(),
}); });
config.set('enterprise.features.logStreaming', true);
const sentryDestination = eventBus.destinations[ const sentryDestination = eventBus.destinations[
testSentryDestination.id! testSentryDestination.id!

View file

@ -17,6 +17,8 @@ import * as testDb from './../shared/testDb';
import type { AuthAgent } from '../shared/types'; import type { AuthAgent } from '../shared/types';
import * as utils from '../shared/utils'; import * as utils from '../shared/utils';
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import Container from 'typedi';
import { License } from '../../../src/License';
jest.mock('@/telemetry'); jest.mock('@/telemetry');
jest.mock('@/UserManagement/email/NodeMailer'); jest.mock('@/UserManagement/email/NodeMailer');
@ -41,6 +43,7 @@ const defaultLdapConfig = {
}; };
beforeAll(async () => { beforeAll(async () => {
Container.get(License).isLdapEnabled = () => true;
app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'] }); app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'] });
const [globalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles(); const [globalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles();
@ -77,10 +80,10 @@ beforeEach(async () => {
config.set('userManagement.disabled', false); config.set('userManagement.disabled', false);
config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
config.set('userManagement.emails.mode', ''); config.set('userManagement.emails.mode', '');
config.set('enterprise.features.ldap', true);
}); });
afterAll(async () => { afterAll(async () => {
Container.reset();
await testDb.terminate(); await testDb.terminate();
}); });

View file

@ -7,22 +7,25 @@ import { randomEmail, randomName, randomValidPassword } from '../shared/random';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
import * as utils from '../shared/utils'; import * as utils from '../shared/utils';
import { sampleConfig } from './sampleMetadata'; import { sampleConfig } from './sampleMetadata';
import Container from 'typedi';
import { License } from '../../../src/License';
let owner: User; let owner: User;
let authOwnerAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest;
async function enableSaml(enable: boolean) { async function enableSaml(enable: boolean) {
await setSamlLoginEnabled(enable); await setSamlLoginEnabled(enable);
config.set('enterprise.features.saml', enable);
} }
beforeAll(async () => { beforeAll(async () => {
Container.get(License).isSamlEnabled = () => true;
const app = await utils.initTestServer({ endpointGroups: ['me', 'saml'] }); const app = await utils.initTestServer({ endpointGroups: ['me', 'saml'] });
owner = await testDb.createOwner(); owner = await testDb.createOwner();
authOwnerAgent = utils.createAuthAgent(app)(owner); authOwnerAgent = utils.createAuthAgent(app)(owner);
}); });
afterAll(async () => { afterAll(async () => {
Container.reset();
await testDb.terminate(); await testDb.terminate();
}); });

View file

@ -74,13 +74,13 @@ import { InternalHooks } from '@/InternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import { LdapManager } from '@/Ldap/LdapManager.ee'; import { LdapManager } from '@/Ldap/LdapManager.ee';
import { LDAP_ENABLED } from '@/Ldap/constants';
import { handleLdapInit } from '@/Ldap/helpers'; import { handleLdapInit } from '@/Ldap/helpers';
import { Push } from '@/push'; import { Push } from '@/push';
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
import { SamlService } from '@/sso/saml/saml.service.ee'; import { SamlService } from '@/sso/saml/saml.service.ee';
import { SamlController } from '@/sso/saml/routes/saml.controller.ee'; import { SamlController } from '@/sso/saml/routes/saml.controller.ee';
import { EventBusController } from '@/eventbus/eventBus.controller'; import { EventBusController } from '@/eventbus/eventBus.controller';
import { License } from '../../../src/License';
export const mockInstance = <T>( export const mockInstance = <T>(
ctor: new (...args: any[]) => T, ctor: new (...args: any[]) => T,
@ -186,7 +186,7 @@ export async function initTestServer({
); );
break; break;
case 'ldap': case 'ldap':
config.set(LDAP_ENABLED, true); Container.get(License).isLdapEnabled = () => true;
await handleLdapInit(); await handleLdapInit();
const { service, sync } = LdapManager.getInstance(); const { service, sync } = LdapManager.getInstance();
registerController( registerController(

View file

@ -12,6 +12,8 @@ import { createWorkflow } from './shared/testDb';
import type { SaveCredentialFunction } from './shared/types'; import type { SaveCredentialFunction } from './shared/types';
import { makeWorkflow } from './shared/utils'; import { makeWorkflow } from './shared/utils';
import { randomCredentialPayload } from './shared/random'; import { randomCredentialPayload } from './shared/random';
import Container from 'typedi';
import { License } from '../../src/License';
let owner: User; let owner: User;
let member: User; let member: User;
@ -23,6 +25,7 @@ let saveCredential: SaveCredentialFunction;
let sharingSpy: jest.SpyInstance<boolean>; let sharingSpy: jest.SpyInstance<boolean>;
beforeAll(async () => { beforeAll(async () => {
Container.get(License).isSharingEnabled = () => true;
const app = await utils.initTestServer({ endpointGroups: ['workflows'] }); const app = await utils.initTestServer({ endpointGroups: ['workflows'] });
const globalOwnerRole = await testDb.getGlobalOwnerRole(); const globalOwnerRole = await testDb.getGlobalOwnerRole();
@ -42,8 +45,6 @@ beforeAll(async () => {
sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
await utils.initNodeTypes(); await utils.initNodeTypes();
config.set('enterprise.features.sharing', true);
}); });
beforeEach(async () => { beforeEach(async () => {
@ -51,6 +52,7 @@ beforeEach(async () => {
}); });
afterAll(async () => { afterAll(async () => {
Container.reset();
await testDb.terminate(); await testDb.terminate();
}); });