feat(core): Add license support to n8n (#4566)

* add sdk

* add license manager

* type fix

* add basic func

* store to db

* update default

* activate license

* add sharing flag

* fix setup

* clear license

* update conosle log to info

* refactor

* use npm dependency

* update error logs

* add simple test

* add license tests

* update tests

* update pnpm package

* fix error handling types

* Update packages/cli/src/config/schema.ts

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

* make feature enum

* add warning

* update sdk

* Update packages/cli/src/config/schema.ts

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

Co-authored-by: Cornelius Suermann <cornelius@n8n.io>
This commit is contained in:
Mutasem Aldmour
2022-11-21 15:41:24 +01:00
committed by GitHub
parent a9bdc0bbfe
commit 30e5d3d04c
9 changed files with 328 additions and 2 deletions

121
packages/cli/src/License.ts Normal file
View File

@@ -0,0 +1,121 @@
import { LicenseManager, TLicenseContainerStr } from '@n8n_io/license-sdk';
import { ILogger } from 'n8n-workflow';
import { getLogger } from './Logger';
import config from '@/config';
import * as Db from '@/Db';
import { LICENSE_FEATURES, SETTINGS_LICENSE_CERT_KEY } from './constants';
async function loadCertStr(): Promise<TLicenseContainerStr> {
const databaseSettings = await Db.collections.Settings.findOne({
where: {
key: SETTINGS_LICENSE_CERT_KEY,
},
});
return databaseSettings?.value ?? '';
}
async function saveCertStr(value: TLicenseContainerStr): Promise<void> {
await Db.collections.Settings.upsert(
{
key: SETTINGS_LICENSE_CERT_KEY,
value,
loadOnStartup: false,
},
['key'],
);
}
export class License {
private logger: ILogger;
private manager: LicenseManager | undefined;
constructor() {
this.logger = getLogger();
}
async init(instanceId: string, version: string) {
if (this.manager) {
return;
}
const server = config.getEnv('license.serverUrl');
const autoRenewEnabled = config.getEnv('license.autoRenewEnabled');
const autoRenewOffset = config.getEnv('license.autoRenewOffset');
try {
this.manager = new LicenseManager({
server,
tenantId: 1,
productIdentifier: `n8n-${version}`,
autoRenewEnabled,
autoRenewOffset,
logger: this.logger,
loadCertStr,
saveCertStr,
deviceFingerprint: () => instanceId,
});
await this.manager.initialize();
} catch (e: unknown) {
if (e instanceof Error) {
this.logger.error('Could not initialize license manager sdk', e);
}
}
}
async activate(activationKey: string): Promise<void> {
if (!this.manager) {
return;
}
if (this.manager.isValid()) {
return;
}
try {
await this.manager.activate(activationKey);
} catch (e) {
if (e instanceof Error) {
this.logger.error('Could not activate license', e);
}
}
}
async renew() {
if (!this.manager) {
return;
}
try {
await this.manager.renew();
} catch (e) {
if (e instanceof Error) {
this.logger.error('Could not renew license', e);
}
}
}
isFeatureEnabled(feature: string): boolean {
if (!this.manager) {
return false;
}
return this.manager.hasFeatureEnabled(feature);
}
isSharingEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.SHARING);
}
}
let licenseInstance: License | undefined;
export function getLicense(): License {
if (licenseInstance === undefined) {
licenseInstance = new License();
}
return licenseInstance;
}

View File

@@ -160,6 +160,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
import { ResponseError } from '@/ResponseHelper';
import { toHttpNodeParameters } from '@/CurlConverterHelper';
import { setupErrorMiddleware } from '@/ErrorReporting';
import { getLicense } from '@/License';
require('body-parser-xml')(bodyParser);
@@ -384,6 +385,16 @@ class App {
return this.frontendSettings;
}
async initLicense(): Promise<void> {
const license = getLicense();
await license.init(this.frontendSettings.instanceId, this.frontendSettings.versionCli);
const activationKey = config.getEnv('license.activationKey');
if (activationKey) {
await license.activate(activationKey);
}
}
async config(): Promise<void> {
const enableMetrics = config.getEnv('endpoints.metrics.enable');
let register: Registry;
@@ -406,6 +417,8 @@ class App {
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
await this.initLicense();
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
const ignoredEndpoints = [

View File

@@ -13,6 +13,7 @@ import { Role } from '@db/entities/Role';
import { AuthenticatedRequest } from '@/requests';
import config from '@/config';
import { getWebhookBaseUrl } from '../WebhookHelpers';
import { getLicense } from '@/License';
import { WhereClause } from '@/Interfaces';
export async function getWorkflowOwner(workflowId: string | number): Promise<User> {
@@ -41,7 +42,11 @@ export function isUserManagementEnabled(): boolean {
}
export function isSharingEnabled(): boolean {
return isUserManagementEnabled() && config.getEnv('enterprise.features.sharing');
const license = getLicense();
return (
isUserManagementEnabled() &&
(config.getEnv('enterprise.features.sharing') || license.isSharingEnabled())
);
}
export function isUserManagementDisabled(): boolean {

View File

@@ -0,0 +1,42 @@
import { Command } from '@oclif/command';
import { LoggerProxy } from 'n8n-workflow';
import * as Db from '@/Db';
import { getLogger } from '@/Logger';
import { SETTINGS_LICENSE_CERT_KEY } from '@/constants';
export class ClearLicenseCommand extends Command {
static description = 'Clear license';
static examples = [`$ n8n clear:license`];
async run() {
const logger = getLogger();
LoggerProxy.init(logger);
try {
await Db.init();
console.info('Clearing license from database.');
await Db.collections.Settings.delete({
key: SETTINGS_LICENSE_CERT_KEY,
});
console.info('Done. Restart n8n to take effect.');
} catch (e: unknown) {
console.error('Error updating database. See log messages for details.');
logger.error('\nGOT ERROR');
logger.info('====================================');
if (e instanceof Error) {
logger.error(e.message);
if (e.stack) {
logger.error(e.stack);
}
}
this.exit(1);
}
this.exit();
}
}

View File

@@ -987,4 +987,31 @@ export const schema = {
env: 'N8N_ONBOARDING_CALL_PROMPTS_ENABLED',
},
},
license: {
serverUrl: {
format: String,
default: 'https://license.n8n.io/v1',
env: 'N8N_LICENSE_SERVER_URL',
doc: 'License server url to retrieve license.',
},
autoRenewEnabled: {
format: Boolean,
default: true,
env: 'N8N_LICENSE_AUTO_RENEW_ENABLED',
doc: 'Whether autorenew for licenses is enabled.',
},
autoRenewOffset: {
format: Number,
default: 60 * 60 * 72, // 72 hours
env: 'N8N_LICENSE_AUTO_RENEW_OFFSET',
doc: 'How many seconds before expiry a license should get automatically renewed. ',
},
activationKey: {
format: String,
default: '',
env: 'N8N_LICENSE_ACTIVATION_KEY',
doc: 'Activation key to initialize license',
},
},
};

View File

@@ -41,3 +41,9 @@ export const UNKNOWN_FAILURE_REASON = 'Unknown failure reason';
export const WORKFLOW_REACTIVATE_INITIAL_TIMEOUT = 1000;
export const WORKFLOW_REACTIVATE_MAX_TIMEOUT = 180000;
export const SETTINGS_LICENSE_CERT_KEY = 'license.cert';
export enum LICENSE_FEATURES {
SHARING = 'feat:sharing',
}