refactor(core): Abstract away InstanceSettings and encryptionKey into injectable services (no-changelog) (#7471)

This change ensures that things like `encryptionKey` and `instanceId`
are always available directly where they are needed, instead of passing
them around throughout the code.
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-10-23 13:39:35 +02:00 committed by GitHub
parent 519680c2cf
commit b6de910cbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 501 additions and 1070 deletions

View file

@ -6,7 +6,11 @@ module.exports = {
}, },
globalSetup: '<rootDir>/test/setup.ts', globalSetup: '<rootDir>/test/setup.ts',
globalTeardown: '<rootDir>/test/teardown.ts', globalTeardown: '<rootDir>/test/teardown.ts',
setupFilesAfterEnv: ['<rootDir>/test/setup-mocks.ts', '<rootDir>/test/extend-expect.ts'], setupFilesAfterEnv: [
'<rootDir>/test/setup-test-folder.ts',
'<rootDir>/test/setup-mocks.ts',
'<rootDir>/test/extend-expect.ts',
],
coveragePathIgnorePatterns: ['/src/databases/migrations/'], coveragePathIgnorePatterns: ['/src/databases/migrations/'],
testTimeout: 10_000, testTimeout: 10_000,
}; };

View file

@ -121,7 +121,6 @@
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"convict": "^6.2.4", "convict": "^6.2.4",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"crypto-js": "~4.1.1",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"curlconverter": "3.21.0", "curlconverter": "3.21.0",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",

View file

@ -45,8 +45,6 @@ export abstract class AbstractServer {
protected endpointWebhookWaiting: string; protected endpointWebhookWaiting: string;
protected instanceId = '';
protected webhooksEnabled = true; protected webhooksEnabled = true;
protected testWebhooksEnabled = false; protected testWebhooksEnabled = false;

View file

@ -1,7 +1,8 @@
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { mkdir, utimes, open, rm } from 'fs/promises'; import { mkdir, utimes, open, rm } from 'fs/promises';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import { UserSettings } from 'n8n-core'; import { Container } from 'typedi';
import { InstanceSettings } from 'n8n-core';
import { LoggerProxy, sleep } from 'n8n-workflow'; import { LoggerProxy, sleep } from 'n8n-workflow';
import { inProduction } from '@/constants'; import { inProduction } from '@/constants';
@ -16,7 +17,8 @@ export const touchFile = async (filePath: string): Promise<void> => {
} }
}; };
const journalFile = join(UserSettings.getUserN8nFolderPath(), 'crash.journal'); const { n8nFolder } = Container.get(InstanceSettings);
const journalFile = join(n8nFolder, 'crash.journal');
export const init = async () => { export const init = async () => {
if (!inProduction) return; if (!inProduction) return;

View file

@ -3,7 +3,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
import { Credentials, NodeExecuteFunctions } from 'n8n-core'; import { Credentials, NodeExecuteFunctions } from 'n8n-core';
import get from 'lodash/get'; import get from 'lodash/get';
@ -53,7 +52,7 @@ import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { whereClause } from './UserManagement/UserManagementHelper'; import { whereClause } from './UserManagement/UserManagementHelper';
import { RESPONSE_ERROR_MESSAGES } from './constants'; import { RESPONSE_ERROR_MESSAGES } from './constants';
import { Container } from 'typedi'; import { Service } from 'typedi';
import { isObjectLiteral } from './utils'; import { isObjectLiteral } from './utils';
const { OAUTH2_CREDENTIAL_TEST_SUCCEEDED, OAUTH2_CREDENTIAL_TEST_FAILED } = RESPONSE_ERROR_MESSAGES; const { OAUTH2_CREDENTIAL_TEST_SUCCEEDED, OAUTH2_CREDENTIAL_TEST_FAILED } = RESPONSE_ERROR_MESSAGES;
@ -87,12 +86,15 @@ const mockNodeTypes: INodeTypes = {
}, },
}; };
@Service()
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
private credentialTypes = Container.get(CredentialTypes); constructor(
private readonly credentialTypes: CredentialTypes,
private nodeTypes = Container.get(NodeTypes); private readonly nodeTypes: NodeTypes,
private readonly credentialsOverwrites: CredentialsOverwrites,
private credentialsOverwrites = Container.get(CredentialsOverwrites); ) {
super();
}
/** /**
* Add the required authentication information to the request * Add the required authentication information to the request
@ -349,7 +351,7 @@ export class CredentialsHelper extends ICredentialsHelper {
expressionResolveValues?: ICredentialsExpressionResolveValues, expressionResolveValues?: ICredentialsExpressionResolveValues,
): Promise<ICredentialDataDecryptedObject> { ): Promise<ICredentialDataDecryptedObject> {
const credentials = await this.getCredentials(nodeCredentials, type); const credentials = await this.getCredentials(nodeCredentials, type);
const decryptedDataOriginal = credentials.getData(this.encryptionKey); const decryptedDataOriginal = credentials.getData();
if (raw === true) { if (raw === true) {
return decryptedDataOriginal; return decryptedDataOriginal;
@ -469,7 +471,7 @@ export class CredentialsHelper extends ICredentialsHelper {
): Promise<void> { ): Promise<void> {
const credentials = await this.getCredentials(nodeCredentials, type); const credentials = await this.getCredentials(nodeCredentials, type);
credentials.setData(data, this.encryptionKey); credentials.setData(data);
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
// Add special database related data // Add special database related data

View file

@ -5,13 +5,12 @@ import type {
SecretsProviderSettings, SecretsProviderSettings,
} from '@/Interfaces'; } from '@/Interfaces';
import { UserSettings } from 'n8n-core'; import { Cipher } from 'n8n-core';
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import { AES, enc } from 'crypto-js';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import type { IDataObject } from 'n8n-workflow'; import { jsonParse, type IDataObject } from 'n8n-workflow';
import { import {
EXTERNAL_SECRETS_INITIAL_BACKOFF, EXTERNAL_SECRETS_INITIAL_BACKOFF,
EXTERNAL_SECRETS_MAX_BACKOFF, EXTERNAL_SECRETS_MAX_BACKOFF,
@ -42,6 +41,7 @@ export class ExternalSecretsManager {
private settingsRepo: SettingsRepository, private settingsRepo: SettingsRepository,
private license: License, private license: License,
private secretsProviders: ExternalSecretsProviders, private secretsProviders: ExternalSecretsProviders,
private cipher: Cipher,
) {} ) {}
async init(): Promise<void> { async init(): Promise<void> {
@ -86,15 +86,10 @@ export class ExternalSecretsManager {
await Container.get(OrchestrationMainService).broadcastReloadExternalSecretsProviders(); await Container.get(OrchestrationMainService).broadcastReloadExternalSecretsProviders();
} }
private async getEncryptionKey(): Promise<string> { private decryptSecretsSettings(value: string): ExternalSecretsSettings {
return UserSettings.getEncryptionKey(); const decryptedData = this.cipher.decrypt(value);
}
private decryptSecretsSettings(value: string, encryptionKey: string): ExternalSecretsSettings {
const decryptedData = AES.decrypt(value, encryptionKey);
try { try {
return JSON.parse(decryptedData.toString(enc.Utf8)) as ExternalSecretsSettings; return jsonParse(decryptedData);
} catch (e) { } catch (e) {
throw new Error( throw new Error(
'External Secrets Settings could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.', 'External Secrets Settings could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
@ -109,8 +104,7 @@ export class ExternalSecretsManager {
if (encryptedSettings === null) { if (encryptedSettings === null) {
return null; return null;
} }
const encryptionKey = await this.getEncryptionKey(); return this.decryptSecretsSettings(encryptedSettings);
return this.decryptSecretsSettings(encryptedSettings, encryptionKey);
} }
private async internalInit() { private async internalInit() {
@ -327,13 +321,12 @@ export class ExternalSecretsManager {
}); });
} }
encryptSecretsSettings(settings: ExternalSecretsSettings, encryptionKey: string): string { private encryptSecretsSettings(settings: ExternalSecretsSettings): string {
return AES.encrypt(JSON.stringify(settings), encryptionKey).toString(); return this.cipher.encrypt(settings);
} }
async saveAndSetSettings(settings: ExternalSecretsSettings, settingsRepo: SettingsRepository) { async saveAndSetSettings(settings: ExternalSecretsSettings, settingsRepo: SettingsRepository) {
const encryptionKey = await this.getEncryptionKey(); const encryptedSettings = this.encryptSecretsSettings(settings);
const encryptedSettings = this.encryptSecretsSettings(settings, encryptionKey);
await settingsRepo.saveEncryptedSecretsProviderSettings(encryptedSettings); await settingsRepo.saveEncryptedSecretsProviderSettings(encryptedSettings);
} }

View file

@ -31,6 +31,7 @@ import { ExecutionRepository } from '@db/repositories';
import { RoleService } from './services/role.service'; import { RoleService } from './services/role.service';
import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow'; import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions'; import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
import { InstanceSettings } from 'n8n-core';
function userToPayload(user: User): { function userToPayload(user: User): {
userId: string; userId: string;
@ -50,22 +51,13 @@ function userToPayload(user: User): {
@Service() @Service()
export class InternalHooks implements IInternalHooksClass { export class InternalHooks implements IInternalHooksClass {
private instanceId: string;
public get telemetryInstanceId(): string {
return this.instanceId;
}
public get telemetryInstance(): Telemetry {
return this.telemetry;
}
constructor( constructor(
private telemetry: Telemetry, private telemetry: Telemetry,
private nodeTypes: NodeTypes, private nodeTypes: NodeTypes,
private roleService: RoleService, private roleService: RoleService,
private executionRepository: ExecutionRepository, private executionRepository: ExecutionRepository,
eventsService: EventsService, eventsService: EventsService,
private readonly instanceSettings: InstanceSettings,
) { ) {
eventsService.on('telemetry.onFirstProductionWorkflowSuccess', async (metrics) => eventsService.on('telemetry.onFirstProductionWorkflowSuccess', async (metrics) =>
this.onFirstProductionWorkflowSuccess(metrics), this.onFirstProductionWorkflowSuccess(metrics),
@ -75,9 +67,7 @@ export class InternalHooks implements IInternalHooksClass {
); );
} }
async init(instanceId: string) { async init() {
this.instanceId = instanceId;
this.telemetry.setInstanceId(instanceId);
await this.telemetry.init(); await this.telemetry.init();
} }
@ -813,7 +803,7 @@ export class InternalHooks implements IInternalHooksClass {
user_id: userCreatedCredentialsData.user.id, user_id: userCreatedCredentialsData.user.id,
credential_type: userCreatedCredentialsData.credential_type, credential_type: userCreatedCredentialsData.credential_type,
credential_id: userCreatedCredentialsData.credential_id, credential_id: userCreatedCredentialsData.credential_id,
instance_id: this.instanceId, instance_id: this.instanceSettings.instanceId,
}), }),
]); ]);
} }
@ -847,7 +837,7 @@ export class InternalHooks implements IInternalHooksClass {
user_id_sharer: userSharedCredentialsData.user_id_sharer, user_id_sharer: userSharedCredentialsData.user_id_sharer,
user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added, user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added,
sharees_removed: userSharedCredentialsData.sharees_removed, sharees_removed: userSharedCredentialsData.sharees_removed,
instance_id: this.instanceId, instance_id: this.instanceSettings.instanceId,
}), }),
]); ]);
} }

View file

@ -1,9 +1,8 @@
/* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable @typescript-eslint/no-use-before-define */
import { AES, enc } from 'crypto-js';
import type { Entry as LdapUser } from 'ldapts'; import type { Entry as LdapUser } from 'ldapts';
import { Filter } from 'ldapts/filters/Filter'; import { Filter } from 'ldapts/filters/Filter';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { UserSettings } from 'n8n-core'; import { Cipher } from 'n8n-core';
import { validate } from 'jsonschema'; import { validate } from 'jsonschema';
import * as Db from '@/Db'; import * as Db from '@/Db';
import config from '@/config'; import config from '@/config';
@ -110,22 +109,6 @@ export const validateLdapConfigurationSchema = (
return { valid, message }; return { valid, message };
}; };
/**
* Encrypt password using the instance's encryption key
*/
export const encryptPassword = async (password: string): Promise<string> => {
const encryptionKey = await UserSettings.getEncryptionKey();
return AES.encrypt(password, encryptionKey).toString();
};
/**
* Decrypt password using the instance's encryption key
*/
export const decryptPassword = async (password: string): Promise<string> => {
const encryptionKey = await UserSettings.getEncryptionKey();
return AES.decrypt(password, encryptionKey).toString(enc.Utf8);
};
/** /**
* Retrieve the LDAP configuration (decrypted) form the database * Retrieve the LDAP configuration (decrypted) form the database
*/ */
@ -134,7 +117,7 @@ export const getLdapConfig = async (): Promise<LdapConfig> => {
key: LDAP_FEATURE_NAME, key: LDAP_FEATURE_NAME,
}); });
const configurationData = jsonParse<LdapConfig>(configuration.value); const configurationData = jsonParse<LdapConfig>(configuration.value);
configurationData.bindingAdminPassword = await decryptPassword( configurationData.bindingAdminPassword = Container.get(Cipher).decrypt(
configurationData.bindingAdminPassword, configurationData.bindingAdminPassword,
); );
return configurationData; return configurationData;
@ -173,7 +156,7 @@ export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> =>
LdapManager.updateConfig({ ...ldapConfig }); LdapManager.updateConfig({ ...ldapConfig });
ldapConfig.bindingAdminPassword = await encryptPassword(ldapConfig.bindingAdminPassword); ldapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(ldapConfig.bindingAdminPassword);
if (!ldapConfig.loginEnabled) { if (!ldapConfig.loginEnabled) {
ldapConfig.synchronizationEnabled = false; ldapConfig.synchronizationEnabled = false;

View file

@ -15,7 +15,7 @@ import Container, { Service } from 'typedi';
import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces'; import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces';
import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher'; import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher';
import { RedisService } from './services/redis.service'; import { RedisService } from './services/redis.service';
import { ObjectStoreService } from 'n8n-core'; import { InstanceSettings, ObjectStoreService } from 'n8n-core';
type FeatureReturnType = Partial< type FeatureReturnType = Partial<
{ {
@ -29,20 +29,17 @@ export class License {
private manager: LicenseManager | undefined; private manager: LicenseManager | undefined;
instanceId: string | undefined;
private redisPublisher: RedisServicePubSubPublisher; private redisPublisher: RedisServicePubSubPublisher;
constructor() { constructor(private readonly instanceSettings: InstanceSettings) {
this.logger = getLogger(); this.logger = getLogger();
} }
async init(instanceId: string, instanceType: N8nInstanceType = 'main') { async init(instanceType: N8nInstanceType = 'main') {
if (this.manager) { if (this.manager) {
return; return;
} }
this.instanceId = instanceId;
const isMainInstance = instanceType === 'main'; const isMainInstance = instanceType === 'main';
const server = config.getEnv('license.serverUrl'); const server = config.getEnv('license.serverUrl');
const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled'); const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled');
@ -67,7 +64,7 @@ export class License {
logger: this.logger, logger: this.logger,
loadCertStr: async () => this.loadCertStr(), loadCertStr: async () => this.loadCertStr(),
saveCertStr, saveCertStr,
deviceFingerprint: () => instanceId, deviceFingerprint: () => this.instanceSettings.instanceId,
onFeatureChange, onFeatureChange,
}); });

View file

@ -6,7 +6,7 @@ import fsPromises from 'fs/promises';
import type { DirectoryLoader, Types } from 'n8n-core'; import type { DirectoryLoader, Types } from 'n8n-core';
import { import {
CUSTOM_EXTENSION_ENV, CUSTOM_EXTENSION_ENV,
UserSettings, InstanceSettings,
CustomDirectoryLoader, CustomDirectoryLoader,
PackageDirectoryLoader, PackageDirectoryLoader,
LazyPackageDirectoryLoader, LazyPackageDirectoryLoader,
@ -47,10 +47,10 @@ export class LoadNodesAndCredentials {
includeNodes = config.getEnv('nodes.include'); includeNodes = config.getEnv('nodes.include');
private downloadFolder: string;
private postProcessors: Array<() => Promise<void>> = []; private postProcessors: Array<() => Promise<void>> = [];
constructor(private readonly instanceSettings: InstanceSettings) {}
async init() { async init() {
if (inTest) throw new Error('Not available in tests'); if (inTest) throw new Error('Not available in tests');
@ -67,8 +67,6 @@ export class LoadNodesAndCredentials {
this.excludeNodes.push('n8n-nodes-base.e2eTest'); this.excludeNodes.push('n8n-nodes-base.e2eTest');
} }
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
// Load nodes from `n8n-nodes-base` // Load nodes from `n8n-nodes-base`
const basePathsToScan = [ const basePathsToScan = [
// In case "n8n" package is in same node_modules folder. // In case "n8n" package is in same node_modules folder.
@ -84,7 +82,9 @@ export class LoadNodesAndCredentials {
// Load nodes from any other `n8n-nodes-*` packages in the download directory // Load nodes from any other `n8n-nodes-*` packages in the download directory
// This includes the community nodes // This includes the community nodes
await this.loadNodesFromNodeModules(path.join(this.downloadFolder, 'node_modules')); await this.loadNodesFromNodeModules(
path.join(this.instanceSettings.nodesDownloadDir, 'node_modules'),
);
await this.loadNodesFromCustomDirectories(); await this.loadNodesFromCustomDirectories();
await this.postProcessLoaders(); await this.postProcessLoaders();
@ -155,7 +155,7 @@ export class LoadNodesAndCredentials {
} }
getCustomDirectories(): string[] { getCustomDirectories(): string[] {
const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()]; const customDirectories = [this.instanceSettings.customExtensionDir];
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) { if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV].split(';'); const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV].split(';');
@ -172,7 +172,11 @@ export class LoadNodesAndCredentials {
} }
async loadPackage(packageName: string) { async loadPackage(packageName: string) {
const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName); const finalNodeUnpackedPath = path.join(
this.instanceSettings.nodesDownloadDir,
'node_modules',
packageName,
);
return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath); return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
} }

View file

@ -1,15 +1,15 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { AES, enc } from 'crypto-js';
import { TOTPService } from './totp.service';
import { Service } from 'typedi'; import { Service } from 'typedi';
import { UserRepository } from '@/databases/repositories'; import { Cipher } from 'n8n-core';
import { UserRepository } from '@db/repositories';
import { TOTPService } from './totp.service';
@Service() @Service()
export class MfaService { export class MfaService {
constructor( constructor(
private userRepository: UserRepository, private userRepository: UserRepository,
public totp: TOTPService, public totp: TOTPService,
private encryptionKey: string, private cipher: Cipher,
) {} ) {}
public generateRecoveryCodes(n = 10) { public generateRecoveryCodes(n = 10) {
@ -17,9 +17,7 @@ export class MfaService {
} }
public generateEncryptedRecoveryCodes() { public generateEncryptedRecoveryCodes() {
return this.generateRecoveryCodes().map((code) => return this.generateRecoveryCodes().map((code) => this.cipher.encrypt(code));
AES.encrypt(code, this.encryptionKey).toString(),
);
} }
public async saveSecretAndRecoveryCodes(userId: string, secret: string, recoveryCodes: string[]) { public async saveSecretAndRecoveryCodes(userId: string, secret: string, recoveryCodes: string[]) {
@ -34,10 +32,8 @@ export class MfaService {
} }
public encryptSecretAndRecoveryCodes(rawSecret: string, rawRecoveryCodes: string[]) { public encryptSecretAndRecoveryCodes(rawSecret: string, rawRecoveryCodes: string[]) {
const encryptedSecret = AES.encrypt(rawSecret, this.encryptionKey).toString(), const encryptedSecret = this.cipher.encrypt(rawSecret),
encryptedRecoveryCodes = rawRecoveryCodes.map((code) => encryptedRecoveryCodes = rawRecoveryCodes.map((code) => this.cipher.encrypt(code));
AES.encrypt(code, this.encryptionKey).toString(),
);
return { return {
encryptedRecoveryCodes, encryptedRecoveryCodes,
encryptedSecret, encryptedSecret,
@ -46,10 +42,8 @@ export class MfaService {
private decryptSecretAndRecoveryCodes(mfaSecret: string, mfaRecoveryCodes: string[]) { private decryptSecretAndRecoveryCodes(mfaSecret: string, mfaRecoveryCodes: string[]) {
return { return {
decryptedSecret: AES.decrypt(mfaSecret, this.encryptionKey).toString(enc.Utf8), decryptedSecret: this.cipher.decrypt(mfaSecret),
decryptedRecoveryCodes: mfaRecoveryCodes.map((code) => decryptedRecoveryCodes: mfaRecoveryCodes.map((code) => this.cipher.decrypt(code)),
AES.decrypt(code, this.encryptionKey).toString(enc.Utf8),
),
}; };
} }
@ -66,7 +60,7 @@ export class MfaService {
} }
public encryptRecoveryCodes(mfaRecoveryCodes: string[]) { public encryptRecoveryCodes(mfaRecoveryCodes: string[]) {
return mfaRecoveryCodes.map((code) => AES.encrypt(code, this.encryptionKey).toString()); return mfaRecoveryCodes.map((code) => this.cipher.encrypt(code));
} }
public async disableMfa(userId: string) { public async disableMfa(userId: string) {

View file

@ -1,4 +1,7 @@
import OTPAuth from 'otpauth'; import OTPAuth from 'otpauth';
import { Service } from 'typedi';
@Service()
export class TOTPService { export class TOTPService {
generateSecret(): string { generateSecret(): string {
return new OTPAuth.Secret()?.base32; return new OTPAuth.Secret()?.base32;

View file

@ -93,7 +93,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
const schema = new CredentialsHelper('') const schema = Container.get(CredentialsHelper)
.getCredentialsProperties(credentialTypeName) .getCredentialsProperties(credentialTypeName)
.filter((property) => property.type !== 'hidden'); .filter((property) => property.type !== 'hidden');

View file

@ -30,7 +30,7 @@ export const validCredentialsProperties = (
): express.Response | void => { ): express.Response | void => {
const { type, data } = req.body; const { type, data } = req.body;
const properties = new CredentialsHelper('') const properties = Container.get(CredentialsHelper)
.getCredentialsProperties(type) .getCredentialsProperties(type)
.filter((property) => property.type !== 'hidden'); .filter((property) => property.type !== 'hidden');

View file

@ -1,4 +1,4 @@
import { UserSettings, Credentials } from 'n8n-core'; import { Credentials } from 'n8n-core';
import type { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow'; import type { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { ICredentialsDb } from '@/Interfaces'; import type { ICredentialsDb } from '@/Interfaces';
@ -87,8 +87,6 @@ export async function removeCredential(credentials: CredentialsEntity): Promise<
} }
export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> { export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> {
const encryptionKey = await UserSettings.getEncryptionKey();
// Encrypt the data // Encrypt the data
const coreCredential = new Credentials( const coreCredential = new Credentials(
{ id: null, name: credential.name }, { id: null, name: credential.name },
@ -97,7 +95,7 @@ export async function encryptCredential(credential: CredentialsEntity): Promise<
); );
// @ts-ignore // @ts-ignore
coreCredential.setData(credential.data, encryptionKey); coreCredential.setData(credential.data);
return coreCredential.getDataToSave() as ICredentialsDb; return coreCredential.getDataToSave() as ICredentialsDb;
} }

View file

@ -30,7 +30,6 @@ import {
LoadMappingOptions, LoadMappingOptions,
LoadNodeParameterOptions, LoadNodeParameterOptions,
LoadNodeListSearch, LoadNodeListSearch,
UserSettings,
} from 'n8n-core'; } from 'n8n-core';
import type { import type {
@ -146,7 +145,6 @@ import { SourceControlService } from '@/environments/sourceControl/sourceControl
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee'; import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
import { ExecutionRepository, SettingsRepository } from '@db/repositories'; import { ExecutionRepository, SettingsRepository } from '@db/repositories';
import type { ExecutionEntity } from '@db/entities/ExecutionEntity'; import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { TOTPService } from './Mfa/totp.service';
import { MfaService } from './Mfa/mfa.service'; import { MfaService } from './Mfa/mfa.service';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers'; import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
import type { FrontendService } from './services/frontend.service'; import type { FrontendService } from './services/frontend.service';
@ -159,25 +157,25 @@ import { WorkflowHistoryController } from './workflows/workflowHistory/workflowH
const exec = promisify(callbackExec); const exec = promisify(callbackExec);
export class Server extends AbstractServer { export class Server extends AbstractServer {
endpointPresetCredentials: string; private endpointPresetCredentials: string;
waitTracker: WaitTracker; private waitTracker: WaitTracker;
activeExecutionsInstance: ActiveExecutions; private activeExecutionsInstance: ActiveExecutions;
presetCredentialsLoaded: boolean; private presetCredentialsLoaded: boolean;
loadNodesAndCredentials: LoadNodesAndCredentials; private loadNodesAndCredentials: LoadNodesAndCredentials;
nodeTypes: NodeTypes; private nodeTypes: NodeTypes;
credentialTypes: ICredentialTypes; private credentialTypes: ICredentialTypes;
frontendService: FrontendService; private frontendService: FrontendService;
postHog: PostHogClient; private postHog: PostHogClient;
push: Push; private push: Push;
constructor() { constructor() {
super('main'); super('main');
@ -285,15 +283,13 @@ export class Server extends AbstractServer {
const repositories = Db.collections; const repositories = Db.collections;
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint); setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
const encryptionKey = await UserSettings.getEncryptionKey();
const logger = LoggerProxy; const logger = LoggerProxy;
const internalHooks = Container.get(InternalHooks); const internalHooks = Container.get(InternalHooks);
const mailer = Container.get(UserManagementMailer); const mailer = Container.get(UserManagementMailer);
const userService = Container.get(UserService); const userService = Container.get(UserService);
const jwtService = Container.get(JwtService); const jwtService = Container.get(JwtService);
const postHog = this.postHog; const postHog = this.postHog;
const mfaService = new MfaService(repositories.User, new TOTPService(), encryptionKey); const mfaService = Container.get(MfaService);
const controllers: object[] = [ const controllers: object[] = [
new EventBusController(), new EventBusController(),
@ -376,19 +372,16 @@ export class Server extends AbstractServer {
await Container.get(MetricsService).configureMetrics(this.app); await Container.get(MetricsService).configureMetrics(this.app);
} }
this.instanceId = await UserSettings.getInstanceId();
this.frontendService.addToSettings({ this.frontendService.addToSettings({
isNpmAvailable: await exec('npm --version') isNpmAvailable: await exec('npm --version')
.then(() => true) .then(() => true)
.catch(() => false), .catch(() => false),
versionCli: N8N_VERSION, versionCli: N8N_VERSION,
instanceId: this.instanceId,
}); });
await this.externalHooks.run('frontend.settings', [this.frontendService.getSettings()]); await this.externalHooks.run('frontend.settings', [this.frontendService.getSettings()]);
await this.postHog.init(this.instanceId); await this.postHog.init();
const publicApiEndpoint = config.getEnv('publicApi.path'); const publicApiEndpoint = config.getEnv('publicApi.path');
const excludeEndpoints = config.getEnv('security.excludeEndpoints'); const excludeEndpoints = config.getEnv('security.excludeEndpoints');
@ -739,18 +732,11 @@ export class Server extends AbstractServer {
throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL); throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL);
} }
let encryptionKey: string;
try {
encryptionKey = await UserSettings.getEncryptionKey();
} catch (error) {
throw new ResponseHelper.InternalServerError(error.message);
}
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id); const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
const mode: WorkflowExecuteMode = 'internal'; const mode: WorkflowExecuteMode = 'internal';
const timezone = config.getEnv('generic.timezone'); const timezone = config.getEnv('generic.timezone');
const credentialsHelper = new CredentialsHelper(encryptionKey); const credentialsHelper = Container.get(CredentialsHelper);
const decryptedDataOriginal = await credentialsHelper.getDecrypted( const decryptedDataOriginal = await credentialsHelper.getDecrypted(
additionalData, additionalData,
credential as INodeCredentialsDetails, credential as INodeCredentialsDetails,
@ -835,7 +821,7 @@ export class Server extends AbstractServer {
credential.nodesAccess, credential.nodesAccess,
); );
credentials.setData(decryptedDataOriginal, encryptionKey); credentials.setData(decryptedDataOriginal);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data // Add special database related data
@ -889,18 +875,11 @@ export class Server extends AbstractServer {
return ResponseHelper.sendErrorResponse(res, errorResponse); return ResponseHelper.sendErrorResponse(res, errorResponse);
} }
let encryptionKey: string;
try {
encryptionKey = await UserSettings.getEncryptionKey();
} catch (error) {
throw new ResponseHelper.InternalServerError(error.message);
}
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id); const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
const mode: WorkflowExecuteMode = 'internal'; const mode: WorkflowExecuteMode = 'internal';
const timezone = config.getEnv('generic.timezone'); const timezone = config.getEnv('generic.timezone');
const credentialsHelper = new CredentialsHelper(encryptionKey); const credentialsHelper = Container.get(CredentialsHelper);
const decryptedDataOriginal = await credentialsHelper.getDecrypted( const decryptedDataOriginal = await credentialsHelper.getDecrypted(
additionalData, additionalData,
credential as INodeCredentialsDetails, credential as INodeCredentialsDetails,
@ -952,7 +931,7 @@ export class Server extends AbstractServer {
credential.type, credential.type,
credential.nodesAccess, credential.nodesAccess,
); );
credentials.setData(decryptedDataOriginal, encryptionKey); credentials.setData(decryptedDataOriginal);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data // Add special database related data
newCredentialsData.updatedAt = new Date(); newCredentialsData.updatedAt = new Date();

View file

@ -1,15 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable id-denylist */ /* eslint-disable id-denylist */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { UserSettings, WorkflowExecute } from 'n8n-core'; import { WorkflowExecute } from 'n8n-core';
import type { import type {
IDataObject, IDataObject,
@ -1033,14 +1029,10 @@ export async function getBase(
const webhookWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookWaiting'); const webhookWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookWaiting');
const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest'); const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest');
const [encryptionKey, variables] = await Promise.all([ const variables = await WorkflowHelpers.getVariables();
UserSettings.getEncryptionKey(),
WorkflowHelpers.getVariables(),
]);
return { return {
credentialsHelper: new CredentialsHelper(encryptionKey), credentialsHelper: Container.get(CredentialsHelper),
encryptionKey,
executeWorkflow, executeWorkflow,
restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'), restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'),
timezone, timezone,

View file

@ -10,7 +10,7 @@ import { setDefaultResultOrder } from 'dns';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { IProcessMessage } from 'n8n-core'; import type { IProcessMessage } from 'n8n-core';
import { BinaryDataService, UserSettings, WorkflowExecute } from 'n8n-core'; import { BinaryDataService, WorkflowExecute } from 'n8n-core';
import type { import type {
ExecutionError, ExecutionError,
@ -107,8 +107,6 @@ class WorkflowRunnerProcess {
// Init db since we need to read the license. // Init db since we need to read the license.
await Db.init(); await Db.init();
const userSettings = await UserSettings.prepareUserSettings();
const loadNodesAndCredentials = Container.get(LoadNodesAndCredentials); const loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
await loadNodesAndCredentials.init(); await loadNodesAndCredentials.init();
@ -118,15 +116,14 @@ class WorkflowRunnerProcess {
const externalHooks = Container.get(ExternalHooks); const externalHooks = Container.get(ExternalHooks);
await externalHooks.init(); await externalHooks.init();
const instanceId = userSettings.instanceId ?? ''; await Container.get(PostHogClient).init();
await Container.get(PostHogClient).init(instanceId); await Container.get(InternalHooks).init();
await Container.get(InternalHooks).init(instanceId);
const binaryDataConfig = config.getEnv('binaryDataManager'); const binaryDataConfig = config.getEnv('binaryDataManager');
await Container.get(BinaryDataService).init(binaryDataConfig); await Container.get(BinaryDataService).init(binaryDataConfig);
const license = Container.get(License); const license = Container.get(License);
await license.init(instanceId); await license.init();
const workflowSettings = this.data.workflowData.settings ?? {}; const workflowSettings = this.data.workflowData.settings ?? {};

View file

@ -1,5 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import { UserSettings } from 'n8n-core'; import { Container } from 'typedi';
import { InstanceSettings } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import { toFlaggedNode } from '@/audit/utils'; import { toFlaggedNode } from '@/audit/utils';
import { separate } from '@/utils'; import { separate } from '@/utils';
@ -81,7 +82,7 @@ function getUnprotectedWebhookNodes(workflows: WorkflowEntity[]) {
async function getNextVersions(currentVersionName: string) { async function getNextVersions(currentVersionName: string) {
const BASE_URL = config.getEnv('versionNotifications.endpoint'); const BASE_URL = config.getEnv('versionNotifications.endpoint');
const instanceId = await UserSettings.getInstanceId(); const { instanceId } = Container.get(InstanceSettings);
const response = await axios.get<n8n.Version[]>(BASE_URL + currentVersionName, { const response = await axios.get<n8n.Version[]>(BASE_URL + currentVersionName, {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention

View file

@ -2,8 +2,7 @@ import { Command } from '@oclif/command';
import { ExitError } from '@oclif/errors'; import { ExitError } from '@oclif/errors';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import type { IUserSettings } from 'n8n-core'; import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
import { BinaryDataService, ObjectStoreService, UserSettings } from 'n8n-core';
import type { AbstractServer } from '@/AbstractServer'; import type { AbstractServer } from '@/AbstractServer';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import config from '@/config'; import config from '@/config';
@ -30,11 +29,9 @@ export abstract class BaseCommand extends Command {
protected nodeTypes: NodeTypes; protected nodeTypes: NodeTypes;
protected userSettings: IUserSettings; protected instanceSettings: InstanceSettings;
protected instanceId: string; private instanceType: N8nInstanceType = 'main';
instanceType: N8nInstanceType = 'main';
queueModeId: string; queueModeId: string;
@ -48,7 +45,7 @@ export abstract class BaseCommand extends Command {
process.once('SIGINT', async () => this.stopProcess()); process.once('SIGINT', async () => this.stopProcess());
// Make sure the settings exist // Make sure the settings exist
this.userSettings = await UserSettings.prepareUserSettings(); this.instanceSettings = Container.get(InstanceSettings);
await Container.get(LoadNodesAndCredentials).init(); await Container.get(LoadNodesAndCredentials).init();
this.nodeTypes = Container.get(NodeTypes); this.nodeTypes = Container.get(NodeTypes);
@ -76,9 +73,8 @@ export abstract class BaseCommand extends Command {
); );
} }
this.instanceId = this.userSettings.instanceId ?? ''; await Container.get(PostHogClient).init();
await Container.get(PostHogClient).init(this.instanceId); await Container.get(InternalHooks).init();
await Container.get(InternalHooks).init(this.instanceId);
} }
protected setInstanceType(instanceType: N8nInstanceType) { protected setInstanceType(instanceType: N8nInstanceType) {
@ -241,7 +237,7 @@ export abstract class BaseCommand extends Command {
async initLicense(): Promise<void> { async initLicense(): Promise<void> {
const license = Container.get(License); const license = Container.get(License);
await license.init(this.instanceId, this.instanceType ?? 'main'); await license.init(this.instanceType ?? 'main');
const activationKey = config.getEnv('license.activationKey'); const activationKey = config.getEnv('license.activationKey');

View file

@ -2,7 +2,7 @@ import { flags } from '@oclif/command';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
import { Credentials, UserSettings } from 'n8n-core'; import { Credentials } from 'n8n-core';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces'; import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
@ -113,13 +113,11 @@ export class ExportCredentialsCommand extends BaseCommand {
const credentials: ICredentialsDb[] = await Db.collections.Credentials.findBy(findQuery); const credentials: ICredentialsDb[] = await Db.collections.Credentials.findBy(findQuery);
if (flags.decrypted) { if (flags.decrypted) {
const encryptionKey = await UserSettings.getEncryptionKey();
for (let i = 0; i < credentials.length; i++) { for (let i = 0; i < credentials.length; i++) {
const { name, type, nodesAccess, data } = credentials[i]; const { name, type, nodesAccess, data } = credentials[i];
const id = credentials[i].id; const id = credentials[i].id;
const credential = new Credentials({ id, name }, type, nodesAccess, data); const credential = new Credentials({ id, name }, type, nodesAccess, data);
const plainData = credential.getData(encryptionKey); const plainData = credential.getData();
(credentials[i] as ICredentialsDecryptedDb).data = plainData; (credentials[i] as ICredentialsDecryptedDb).data = plainData;
} }
} }

View file

@ -72,8 +72,6 @@ export class ImportCredentialsCommand extends BaseCommand {
await this.initOwnerCredentialRole(); await this.initOwnerCredentialRole();
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
const encryptionKey = this.userSettings.encryptionKey;
if (flags.separate) { if (flags.separate) {
let { input: inputPath } = flags; let { input: inputPath } = flags;
@ -97,7 +95,7 @@ export class ImportCredentialsCommand extends BaseCommand {
if (typeof credential.data === 'object') { if (typeof credential.data === 'object') {
// plain data / decrypted input. Should be encrypted first. // plain data / decrypted input. Should be encrypted first.
Credentials.prototype.setData.call(credential, credential.data, encryptionKey); Credentials.prototype.setData.call(credential, credential.data);
} }
await this.storeCredential(credential, user); await this.storeCredential(credential, user);
@ -125,7 +123,7 @@ export class ImportCredentialsCommand extends BaseCommand {
for (const credential of credentials) { for (const credential of credentials) {
if (typeof credential.data === 'object') { if (typeof credential.data === 'object') {
// plain data / decrypted input. Should be encrypted first. // plain data / decrypted input. Should be encrypted first.
Credentials.prototype.setData.call(credential, credential.data, encryptionKey); Credentials.prototype.setData.call(credential, credential.data);
} }
await this.storeCredential(credential, user); await this.storeCredential(credential, user);
} }

View file

@ -9,7 +9,7 @@ export class LicenseInfoCommand extends BaseCommand {
async run() { async run() {
const license = Container.get(License); const license = Container.get(License);
await license.init(this.instanceId); await license.init();
this.logger.info('Printing license information:\n' + license.getInfo()); this.logger.info('Printing license information:\n' + license.getInfo());
} }

View file

@ -6,7 +6,6 @@ import path from 'path';
import { mkdir } from 'fs/promises'; import { mkdir } from 'fs/promises';
import { createReadStream, createWriteStream, existsSync } from 'fs'; import { createReadStream, createWriteStream, existsSync } from 'fs';
import localtunnel from 'localtunnel'; import localtunnel from 'localtunnel';
import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core';
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import stream from 'stream'; import stream from 'stream';
import replaceStream from 'replacestream'; import replaceStream from 'replacestream';
@ -245,7 +244,7 @@ export class Start extends BaseCommand {
if (!config.getEnv('userManagement.jwtSecret')) { if (!config.getEnv('userManagement.jwtSecret')) {
// If we don't have a JWT secret set, generate // If we don't have a JWT secret set, generate
// one based and save to config. // one based and save to config.
const encryptionKey = await UserSettings.getEncryptionKey(); const { encryptionKey } = this.instanceSettings;
// For a key off every other letter from encryption key // For a key off every other letter from encryption key
// CAREFUL: do not change this or it breaks all existing tokens. // CAREFUL: do not change this or it breaks all existing tokens.
@ -256,8 +255,6 @@ export class Start extends BaseCommand {
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex')); config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
} }
await UserSettings.getEncryptionKey();
// Load settings from database and set them to config. // Load settings from database and set them to config.
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true }); const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
databaseSettings.forEach((setting) => { databaseSettings.forEach((setting) => {
@ -285,28 +282,19 @@ export class Start extends BaseCommand {
if (flags.tunnel) { if (flags.tunnel) {
this.log('\nWaiting for tunnel ...'); this.log('\nWaiting for tunnel ...');
let tunnelSubdomain; let tunnelSubdomain =
if ( process.env.N8N_TUNNEL_SUBDOMAIN ?? this.instanceSettings.tunnelSubdomain ?? '';
process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined &&
process.env[TUNNEL_SUBDOMAIN_ENV] !== ''
) {
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
} else if (this.userSettings.tunnelSubdomain !== undefined) {
tunnelSubdomain = this.userSettings.tunnelSubdomain;
}
if (tunnelSubdomain === undefined) { if (tunnelSubdomain === '') {
// When no tunnel subdomain did exist yet create a new random one // When no tunnel subdomain did exist yet create a new random one
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
this.userSettings.tunnelSubdomain = Array.from({ length: 24 }) tunnelSubdomain = Array.from({ length: 24 })
.map(() => { .map(() =>
return availableCharacters.charAt( availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length)),
Math.floor(Math.random() * availableCharacters.length), )
);
})
.join(''); .join('');
await UserSettings.writeUserSettings(this.userSettings); this.instanceSettings.update({ tunnelSubdomain });
} }
const tunnelSettings: localtunnel.TunnelConfig = { const tunnelSettings: localtunnel.TunnelConfig = {

View file

@ -318,7 +318,6 @@ export class Worker extends BaseCommand {
await Container.get(OrchestrationWorkerService).init(); await Container.get(OrchestrationWorkerService).init();
await Container.get(OrchestrationHandlerWorkerService).initWithOptions({ await Container.get(OrchestrationHandlerWorkerService).initWithOptions({
queueModeId: this.queueModeId, queueModeId: this.queueModeId,
instanceId: this.instanceId,
redisPublisher: Container.get(OrchestrationWorkerService).redisPublisher, redisPublisher: Container.get(OrchestrationWorkerService).redisPublisher,
getRunningJobIds: () => Object.keys(Worker.runningJobs), getRunningJobIds: () => Object.keys(Worker.runningJobs),
getRunningJobsSummary: () => Object.values(Worker.runningJobsSummary), getRunningJobsSummary: () => Object.values(Worker.runningJobsSummary),

View file

@ -21,12 +21,9 @@ if (inE2ETests) {
N8N_AI_ENABLED: 'true', N8N_AI_ENABLED: 'true',
}; };
} else if (inTest) { } else if (inTest) {
const testsDir = join(tmpdir(), 'n8n-tests/');
mkdirSync(testsDir, { recursive: true });
process.env.N8N_LOG_LEVEL = 'silent'; process.env.N8N_LOG_LEVEL = 'silent';
process.env.N8N_ENCRYPTION_KEY = 'test-encryption-key'; process.env.N8N_ENCRYPTION_KEY = 'test-encryption-key';
process.env.N8N_PUBLIC_API_DISABLED = 'true'; process.env.N8N_PUBLIC_API_DISABLED = 'true';
process.env.N8N_USER_FOLDER = mkdtempSync(testsDir);
process.env.SKIP_STATISTICS_EVENTS = 'true'; process.env.SKIP_STATISTICS_EVENTS = 'true';
} else { } else {
dotenv.config(); dotenv.config();

View file

@ -1,6 +1,7 @@
import path from 'path'; import path from 'path';
import convict from 'convict'; import convict from 'convict';
import { UserSettings } from 'n8n-core'; import { Container } from 'typedi';
import { InstanceSettings } from 'n8n-core';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import { ensureStringArray } from './utils'; import { ensureStringArray } from './utils';
@ -881,7 +882,7 @@ export const schema = {
location: { location: {
doc: 'Log file location; only used if log output is set to file.', doc: 'Log file location; only used if log output is set to file.',
format: String, format: String,
default: path.join(UserSettings.getUserN8nFolderPath(), 'logs/n8n.log'), default: path.join(Container.get(InstanceSettings).n8nFolder, 'logs/n8n.log'),
env: 'N8N_LOG_FILE_LOCATION', env: 'N8N_LOG_FILE_LOCATION',
}, },
}, },
@ -947,7 +948,7 @@ export const schema = {
}, },
localStoragePath: { localStoragePath: {
format: String, format: String,
default: path.join(UserSettings.getUserN8nFolderPath(), 'binaryData'), default: path.join(Container.get(InstanceSettings).n8nFolder, 'binaryData'),
env: 'N8N_BINARY_DATA_STORAGE_PATH', env: 'N8N_BINARY_DATA_STORAGE_PATH',
doc: 'Path for binary data storage in "filesystem" mode', doc: 'Path for binary data storage in "filesystem" mode',
}, },

View file

@ -1,7 +1,8 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { resolve, join, dirname } from 'path'; import { resolve, join, dirname } from 'path';
import { Container } from 'typedi';
import type { n8n } from 'n8n-core'; import type { n8n } from 'n8n-core';
import { RESPONSE_ERROR_MESSAGES as CORE_RESPONSE_ERROR_MESSAGES, UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
const { NODE_ENV, E2E_TESTS } = process.env; const { NODE_ENV, E2E_TESTS } = process.env;
@ -16,7 +17,10 @@ export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__';
export const CLI_DIR = resolve(__dirname, '..'); export const CLI_DIR = resolve(__dirname, '..');
export const TEMPLATES_DIR = join(CLI_DIR, 'templates'); export const TEMPLATES_DIR = join(CLI_DIR, 'templates');
export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base')); export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base'));
export const GENERATED_STATIC_DIR = join(UserSettings.getUserHome(), '.cache/n8n/public'); export const GENERATED_STATIC_DIR = join(
Container.get(InstanceSettings).userHome,
'.cache/n8n/public',
);
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist'); export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
export function getN8nPackageJson() { export function getN8nPackageJson() {
@ -34,7 +38,6 @@ export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
export const RESPONSE_ERROR_MESSAGES = { export const RESPONSE_ERROR_MESSAGES = {
NO_CREDENTIAL: 'Credential not found', NO_CREDENTIAL: 'Credential not found',
NO_NODE: 'Node not found', NO_NODE: 'Node not found',
NO_ENCRYPTION_KEY: CORE_RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
PACKAGE_NAME_NOT_PROVIDED: 'Package name is required', PACKAGE_NAME_NOT_PROVIDED: 'Package name is required',
PACKAGE_NAME_NOT_VALID: `Package name is not valid - it must start with "${NODE_PACKAGE_PREFIX}"`, PACKAGE_NAME_NOT_VALID: `Package name is not valid - it must start with "${NODE_PACKAGE_PREFIX}"`,
PACKAGE_NOT_INSTALLED: 'This package is not installed - you must install it first', PACKAGE_NOT_INSTALLED: 'This package is not installed - you must install it first',

View file

@ -12,9 +12,7 @@ import { LICENSE_FEATURES, inE2ETests } from '@/constants';
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators'; import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
import type { UserSetupPayload } from '@/requests'; import type { UserSetupPayload } from '@/requests';
import type { BooleanLicenseFeature } from '@/Interfaces'; import type { BooleanLicenseFeature } from '@/Interfaces';
import { UserSettings } from 'n8n-core';
import { MfaService } from '@/Mfa/mfa.service'; import { MfaService } from '@/Mfa/mfa.service';
import { TOTPService } from '@/Mfa/totp.service';
if (!inE2ETests) { if (!inE2ETests) {
console.error('E2E endpoints only allowed during E2E tests'); console.error('E2E endpoints only allowed during E2E tests');
@ -77,6 +75,7 @@ export class E2EController {
private settingsRepo: SettingsRepository, private settingsRepo: SettingsRepository,
private userRepo: UserRepository, private userRepo: UserRepository,
private workflowRunner: ActiveWorkflowRunner, private workflowRunner: ActiveWorkflowRunner,
private mfaService: MfaService,
) { ) {
license.isFeatureEnabled = (feature: BooleanLicenseFeature) => license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
this.enabledFeatures[feature] ?? false; this.enabledFeatures[feature] ?? false;
@ -141,10 +140,6 @@ export class E2EController {
roles.map(([name, scope], index) => ({ name, scope, id: (index + 1).toString() })), roles.map(([name, scope], index) => ({ name, scope, id: (index + 1).toString() })),
); );
const encryptionKey = await UserSettings.getEncryptionKey();
const mfaService = new MfaService(this.userRepo, new TOTPService(), encryptionKey);
const instanceOwner = { const instanceOwner = {
id: uuid(), id: uuid(),
...owner, ...owner,
@ -153,10 +148,8 @@ export class E2EController {
}; };
if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) { if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) {
const { encryptedRecoveryCodes, encryptedSecret } = mfaService.encryptSecretAndRecoveryCodes( const { encryptedRecoveryCodes, encryptedSecret } =
owner.mfaSecret, this.mfaService.encryptSecretAndRecoveryCodes(owner.mfaSecret, owner.mfaRecoveryCodes);
owner.mfaRecoveryCodes,
);
instanceOwner.mfaSecret = encryptedSecret; instanceOwner.mfaSecret = encryptedSecret;
instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes; instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes;
} }

View file

@ -61,11 +61,7 @@ EECredentialsController.get(
const { data: _, ...rest } = credential; const { data: _, ...rest } = credential;
const key = await EECredentials.getEncryptionKey(); const decryptedData = EECredentials.redact(EECredentials.decrypt(credential), credential);
const decryptedData = EECredentials.redact(
await EECredentials.decrypt(key, credential),
credential,
);
return { data: decryptedData, ...rest }; return { data: decryptedData, ...rest };
}), }),
@ -81,8 +77,6 @@ EECredentialsController.post(
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => { ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
const { credentials } = req.body; const { credentials } = req.body;
const encryptionKey = await EECredentials.getEncryptionKey();
const credentialId = credentials.id; const credentialId = credentials.id;
const { ownsCredential } = await EECredentials.isOwned(req.user, credentialId); const { ownsCredential } = await EECredentials.isOwned(req.user, credentialId);
@ -92,17 +86,17 @@ EECredentialsController.post(
throw new ResponseHelper.UnauthorizedError('Forbidden'); throw new ResponseHelper.UnauthorizedError('Forbidden');
} }
const decryptedData = await EECredentials.decrypt(encryptionKey, sharing.credentials); const decryptedData = EECredentials.decrypt(sharing.credentials);
Object.assign(credentials, { data: decryptedData }); Object.assign(credentials, { data: decryptedData });
} }
const mergedCredentials = deepCopy(credentials); const mergedCredentials = deepCopy(credentials);
if (mergedCredentials.data && sharing?.credentials) { if (mergedCredentials.data && sharing?.credentials) {
const decryptedData = await EECredentials.decrypt(encryptionKey, sharing.credentials); const decryptedData = EECredentials.decrypt(sharing.credentials);
mergedCredentials.data = EECredentials.unredact(mergedCredentials.data, decryptedData); mergedCredentials.data = EECredentials.unredact(mergedCredentials.data, decryptedData);
} }
return EECredentials.test(req.user, encryptionKey, mergedCredentials); return EECredentials.test(req.user, mergedCredentials);
}), }),
); );

View file

@ -86,9 +86,8 @@ credentialsController.get(
return { ...rest }; return { ...rest };
} }
const key = await CredentialsService.getEncryptionKey();
const decryptedData = CredentialsService.redact( const decryptedData = CredentialsService.redact(
await CredentialsService.decrypt(key, credential), CredentialsService.decrypt(credential),
credential, credential,
); );
@ -106,16 +105,15 @@ credentialsController.post(
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => { ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
const { credentials } = req.body; const { credentials } = req.body;
const encryptionKey = await CredentialsService.getEncryptionKey();
const sharing = await CredentialsService.getSharing(req.user, credentials.id); const sharing = await CredentialsService.getSharing(req.user, credentials.id);
const mergedCredentials = deepCopy(credentials); const mergedCredentials = deepCopy(credentials);
if (mergedCredentials.data && sharing?.credentials) { if (mergedCredentials.data && sharing?.credentials) {
const decryptedData = await CredentialsService.decrypt(encryptionKey, sharing.credentials); const decryptedData = CredentialsService.decrypt(sharing.credentials);
mergedCredentials.data = CredentialsService.unredact(mergedCredentials.data, decryptedData); mergedCredentials.data = CredentialsService.unredact(mergedCredentials.data, decryptedData);
} }
return CredentialsService.test(req.user, encryptionKey, mergedCredentials); return CredentialsService.test(req.user, mergedCredentials);
}), }),
); );
@ -127,8 +125,7 @@ credentialsController.post(
ResponseHelper.send(async (req: CredentialRequest.Create) => { ResponseHelper.send(async (req: CredentialRequest.Create) => {
const newCredential = await CredentialsService.prepareCreateData(req.body); const newCredential = await CredentialsService.prepareCreateData(req.body);
const key = await CredentialsService.getEncryptionKey(); const encryptedData = CredentialsService.createEncryptedData(null, newCredential);
const encryptedData = CredentialsService.createEncryptedData(key, null, newCredential);
const credential = await CredentialsService.save(newCredential, encryptedData, req.user); const credential = await CredentialsService.save(newCredential, encryptedData, req.user);
void Container.get(InternalHooks).onUserCreatedCredentials({ void Container.get(InternalHooks).onUserCreatedCredentials({
@ -165,14 +162,12 @@ credentialsController.patch(
const { credentials: credential } = sharing; const { credentials: credential } = sharing;
const key = await CredentialsService.getEncryptionKey(); const decryptedData = CredentialsService.decrypt(credential);
const decryptedData = await CredentialsService.decrypt(key, credential);
const preparedCredentialData = await CredentialsService.prepareUpdateData( const preparedCredentialData = await CredentialsService.prepareUpdateData(
req.body, req.body,
decryptedData, decryptedData,
); );
const newCredentialData = CredentialsService.createEncryptedData( const newCredentialData = CredentialsService.createEncryptedData(
key,
credentialId, credentialId,
preparedCredentialData, preparedCredentialData,
); );

View file

@ -1,4 +1,4 @@
import { Credentials, UserSettings } from 'n8n-core'; import { Credentials } from 'n8n-core';
import type { import type {
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
ICredentialsDecrypted, ICredentialsDecrypted,
@ -12,10 +12,9 @@ import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { In, Like } from 'typeorm'; import { In, Like } from 'typeorm';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper';
import type { ICredentialsDb } from '@/Interfaces'; import type { ICredentialsDb } from '@/Interfaces';
import { CredentialsHelper, createCredentialsFromCredentialsEntity } from '@/CredentialsHelper'; import { CredentialsHelper, createCredentialsFromCredentialsEntity } from '@/CredentialsHelper';
import { CREDENTIAL_BLANKING_VALUE, RESPONSE_ERROR_MESSAGES } from '@/constants'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedCredentials } from '@db/entities/SharedCredentials';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
@ -205,18 +204,14 @@ export class CredentialsService {
return updateData; return updateData;
} }
static createEncryptedData( static createEncryptedData(credentialId: string | null, data: CredentialsEntity): ICredentialsDb {
encryptionKey: string,
credentialId: string | null,
data: CredentialsEntity,
): ICredentialsDb {
const credentials = new Credentials( const credentials = new Credentials(
{ id: credentialId, name: data.name }, { id: credentialId, name: data.name },
data.type, data.type,
data.nodesAccess, data.nodesAccess,
); );
credentials.setData(data.data as unknown as ICredentialDataDecryptedObject, encryptionKey); credentials.setData(data.data as unknown as ICredentialDataDecryptedObject);
const newCredentialData = credentials.getDataToSave() as ICredentialsDb; const newCredentialData = credentials.getDataToSave() as ICredentialsDb;
@ -226,22 +221,9 @@ export class CredentialsService {
return newCredentialData; return newCredentialData;
} }
static async getEncryptionKey(): Promise<string> { static decrypt(credential: CredentialsEntity): ICredentialDataDecryptedObject {
try {
return await UserSettings.getEncryptionKey();
} catch (error) {
throw new ResponseHelper.InternalServerError(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
}
}
static async decrypt(
encryptionKey: string,
credential: CredentialsEntity,
): Promise<ICredentialDataDecryptedObject> {
const coreCredential = createCredentialsFromCredentialsEntity(credential); const coreCredential = createCredentialsFromCredentialsEntity(credential);
const data = coreCredential.getData(encryptionKey); return coreCredential.getData();
return data;
} }
static async update( static async update(
@ -303,11 +285,9 @@ export class CredentialsService {
static async test( static async test(
user: User, user: User,
encryptionKey: string,
credentials: ICredentialsDecrypted, credentials: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> { ): Promise<INodeCredentialTestResult> {
const helper = new CredentialsHelper(encryptionKey); const helper = Container.get(CredentialsHelper);
return helper.testCredentials(user, credentials.type, credentials); return helper.testCredentials(user, credentials.type, credentials);
} }

View file

@ -9,12 +9,8 @@ import omit from 'lodash/omit';
import set from 'lodash/set'; import set from 'lodash/set';
import split from 'lodash/split'; import split from 'lodash/split';
import unset from 'lodash/unset'; import unset from 'lodash/unset';
import { Credentials, UserSettings } from 'n8n-core'; import { Credentials } from 'n8n-core';
import type { import type { WorkflowExecuteMode, INodeCredentialsDetails } from 'n8n-workflow';
WorkflowExecuteMode,
INodeCredentialsDetails,
ICredentialsEncrypted,
} from 'n8n-workflow';
import { LoggerProxy, jsonStringify } from 'n8n-workflow'; import { LoggerProxy, jsonStringify } from 'n8n-workflow';
import { resolve as pathResolve } from 'path'; import { resolve as pathResolve } from 'path';
@ -76,20 +72,13 @@ oauth2CredentialController.get(
throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL); throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL);
} }
let encryptionKey: string;
try {
encryptionKey = await UserSettings.getEncryptionKey();
} catch (error) {
throw new ResponseHelper.InternalServerError((error as Error).message);
}
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id); const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
const credentialType = (credential as unknown as ICredentialsEncrypted).type; const credentialType = credential.type;
const mode: WorkflowExecuteMode = 'internal'; const mode: WorkflowExecuteMode = 'internal';
const timezone = config.getEnv('generic.timezone'); const timezone = config.getEnv('generic.timezone');
const credentialsHelper = new CredentialsHelper(encryptionKey); const credentialsHelper = Container.get(CredentialsHelper);
const decryptedDataOriginal = await credentialsHelper.getDecrypted( const decryptedDataOriginal = await credentialsHelper.getDecrypted(
additionalData, additionalData,
credential as INodeCredentialsDetails, credential as INodeCredentialsDetails,
@ -152,7 +141,7 @@ oauth2CredentialController.get(
const credentials = new Credentials( const credentials = new Credentials(
credential as INodeCredentialsDetails, credential as INodeCredentialsDetails,
credentialType, credentialType,
(credential as unknown as ICredentialsEncrypted).nodesAccess, credential.nodesAccess,
); );
decryptedDataOriginal.csrfSecret = csrfSecret; decryptedDataOriginal.csrfSecret = csrfSecret;
@ -166,7 +155,7 @@ oauth2CredentialController.get(
decryptedDataOriginal.codeVerifier = code_verifier; decryptedDataOriginal.codeVerifier = code_verifier;
} }
credentials.setData(decryptedDataOriginal, encryptionKey); credentials.setData(decryptedDataOriginal);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data // Add special database related data
@ -228,16 +217,15 @@ oauth2CredentialController.get(
return renderCallbackError(res, errorMessage); return renderCallbackError(res, errorMessage);
} }
const encryptionKey = await UserSettings.getEncryptionKey();
const additionalData = await WorkflowExecuteAdditionalData.getBase(state.cid); const additionalData = await WorkflowExecuteAdditionalData.getBase(state.cid);
const mode: WorkflowExecuteMode = 'internal'; const mode: WorkflowExecuteMode = 'internal';
const timezone = config.getEnv('generic.timezone'); const timezone = config.getEnv('generic.timezone');
const credentialsHelper = new CredentialsHelper(encryptionKey); const credentialsHelper = Container.get(CredentialsHelper);
const decryptedDataOriginal = await credentialsHelper.getDecrypted( const decryptedDataOriginal = await credentialsHelper.getDecrypted(
additionalData, additionalData,
credential as INodeCredentialsDetails, credential as INodeCredentialsDetails,
(credential as unknown as ICredentialsEncrypted).type, credential.type,
mode, mode,
timezone, timezone,
true, true,
@ -245,7 +233,7 @@ oauth2CredentialController.get(
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites( const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
additionalData, additionalData,
decryptedDataOriginal, decryptedDataOriginal,
(credential as unknown as ICredentialsEncrypted).type, credential.type,
mode, mode,
timezone, timezone,
); );
@ -330,10 +318,10 @@ oauth2CredentialController.get(
const credentials = new Credentials( const credentials = new Credentials(
credential as INodeCredentialsDetails, credential as INodeCredentialsDetails,
(credential as unknown as ICredentialsEncrypted).type, credential.type,
(credential as unknown as ICredentialsEncrypted).nodesAccess, credential.nodesAccess,
); );
credentials.setData(decryptedDataOriginal, encryptionKey); credentials.setData(decryptedDataOriginal);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data // Add special database related data
newCredentialsData.updatedAt = new Date(); newCredentialsData.updatedAt = new Date();

View file

@ -1,8 +1,9 @@
import path from 'path'; import path from 'path';
import { Container } from 'typedi';
import type { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'; import type { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
import type { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'; import type { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions';
import { UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { entities } from './entities'; import { entities } from './entities';
import { mysqlMigrations } from './migrations/mysqldb'; import { mysqlMigrations } from './migrations/mysqldb';
@ -21,7 +22,7 @@ const getDBConnectionOptions = (dbType: DatabaseType) => {
configDBType === 'sqlite' configDBType === 'sqlite'
? { ? {
database: path.resolve( database: path.resolve(
UserSettings.getUserN8nFolderPath(), Container.get(InstanceSettings).n8nFolder,
config.getEnv('database.sqlite.database'), config.getEnv('database.sqlite.database'),
), ),
enableWAL: config.getEnv('database.sqlite.enableWAL'), enableWAL: config.getEnv('database.sqlite.enableWAL'),

View file

@ -1,6 +1,7 @@
import { statSync } from 'fs'; import { statSync } from 'fs';
import path from 'path'; import path from 'path';
import { UserSettings } from 'n8n-core'; import { Container } from 'typedi';
import { InstanceSettings } from 'n8n-core';
import type { MigrationContext, IrreversibleMigration } from '@db/types'; import type { MigrationContext, IrreversibleMigration } from '@db/types';
import config from '@/config'; import config from '@/config';
@ -191,7 +192,7 @@ const migrationsPruningEnabled = process.env.MIGRATIONS_PRUNING_ENABLED === 'tru
function getSqliteDbFileSize(): number { function getSqliteDbFileSize(): number {
const filename = path.resolve( const filename = path.resolve(
UserSettings.getUserN8nFolderPath(), Container.get(InstanceSettings).n8nFolder,
config.getEnv('database.sqlite.database'), config.getEnv('database.sqlite.database'),
); );
const { size } = statSync(filename); const { size } = statSync(filename);

View file

@ -1,6 +1,6 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { readFileSync, rmSync } from 'fs'; import { readFileSync, rmSync } from 'fs';
import { UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import type { ObjectLiteral } from 'typeorm'; import type { ObjectLiteral } from 'typeorm';
import type { QueryRunner } from 'typeorm/query-runner/QueryRunner'; import type { QueryRunner } from 'typeorm/query-runner/QueryRunner';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
@ -16,9 +16,10 @@ const logger = getLogger();
const PERSONALIZATION_SURVEY_FILENAME = 'personalizationSurvey.json'; const PERSONALIZATION_SURVEY_FILENAME = 'personalizationSurvey.json';
function loadSurveyFromDisk(): string | null { function loadSurveyFromDisk(): string | null {
const userSettingsPath = UserSettings.getUserN8nFolderPath();
try { try {
const filename = `${userSettingsPath}/${PERSONALIZATION_SURVEY_FILENAME}`; const filename = `${
Container.get(InstanceSettings).n8nFolder
}/${PERSONALIZATION_SURVEY_FILENAME}`;
const surveyFile = readFileSync(filename, 'utf-8'); const surveyFile = readFileSync(filename, 'utf-8');
rmSync(filename); rmSync(filename);
const personalizationSurvey = JSON.parse(surveyFile) as object; const personalizationSurvey = JSON.parse(surveyFile) as object;

View file

@ -12,14 +12,10 @@ import type { SourceControlPreferences } from './types/sourceControlPreferences'
import { import {
SOURCE_CONTROL_DEFAULT_EMAIL, SOURCE_CONTROL_DEFAULT_EMAIL,
SOURCE_CONTROL_DEFAULT_NAME, SOURCE_CONTROL_DEFAULT_NAME,
SOURCE_CONTROL_GIT_FOLDER,
SOURCE_CONTROL_README, SOURCE_CONTROL_README,
SOURCE_CONTROL_SSH_FOLDER,
SOURCE_CONTROL_SSH_KEY_NAME,
} from './constants'; } from './constants';
import { LoggerProxy } from 'n8n-workflow'; import { LoggerProxy } from 'n8n-workflow';
import { SourceControlGitService } from './sourceControlGit.service.ee'; import { SourceControlGitService } from './sourceControlGit.service.ee';
import { UserSettings } from 'n8n-core';
import type { PushResult } from 'simple-git'; import type { PushResult } from 'simple-git';
import { SourceControlExportService } from './sourceControlExport.service.ee'; import { SourceControlExportService } from './sourceControlExport.service.ee';
import { BadRequestError } from '@/ResponseHelper'; import { BadRequestError } from '@/ResponseHelper';
@ -55,10 +51,10 @@ export class SourceControlService {
private sourceControlImportService: SourceControlImportService, private sourceControlImportService: SourceControlImportService,
private tagRepository: TagRepository, private tagRepository: TagRepository,
) { ) {
const userFolder = UserSettings.getUserN8nFolderPath(); const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService;
this.sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER); this.gitFolder = gitFolder;
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER); this.sshFolder = sshFolder;
this.sshKeyName = path.join(this.sshFolder, SOURCE_CONTROL_SSH_KEY_NAME); this.sshKeyName = sshKeyName;
} }
async init(): Promise<void> { async init(): Promise<void> {

View file

@ -11,7 +11,7 @@ import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { LoggerProxy } from 'n8n-workflow'; import { LoggerProxy } from 'n8n-workflow';
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises'; import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises';
import { rmSync } from 'fs'; import { rmSync } from 'fs';
import { Credentials, UserSettings } from 'n8n-core'; import { Credentials, InstanceSettings } from 'n8n-core';
import type { ExportableWorkflow } from './types/exportableWorkflow'; import type { ExportableWorkflow } from './types/exportableWorkflow';
import type { ExportableCredential } from './types/exportableCredential'; import type { ExportableCredential } from './types/exportableCredential';
import type { ExportResult } from './types/exportResult'; import type { ExportResult } from './types/exportResult';
@ -39,9 +39,9 @@ export class SourceControlExportService {
constructor( constructor(
private readonly variablesService: VariablesService, private readonly variablesService: VariablesService,
private readonly tagRepository: TagRepository, private readonly tagRepository: TagRepository,
instanceSettings: InstanceSettings,
) { ) {
const userFolder = UserSettings.getUserN8nFolderPath(); this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER); this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
this.credentialExportFolder = path.join( this.credentialExportFolder = path.join(
this.gitFolder, this.gitFolder,
@ -248,12 +248,11 @@ export class SourceControlExportService {
(remote) => foundCredentialIds.findIndex((local) => local === remote) === -1, (remote) => foundCredentialIds.findIndex((local) => local === remote) === -1,
); );
} }
const encryptionKey = await UserSettings.getEncryptionKey();
await Promise.all( await Promise.all(
credentialsToBeExported.map(async (sharedCredential) => { credentialsToBeExported.map(async (sharedCredential) => {
const { name, type, nodesAccess, data, id } = sharedCredential.credentials; const { name, type, nodesAccess, data, id } = sharedCredential.credentials;
const credentialObject = new Credentials({ id, name }, type, nodesAccess, data); const credentialObject = new Credentials({ id, name }, type, nodesAccess, data);
const plainData = credentialObject.getData(encryptionKey); const plainData = credentialObject.getData();
const sanitizedData = this.replaceCredentialData(plainData); const sanitizedData = this.replaceCredentialData(plainData);
const fileName = this.getCredentialsPath(sharedCredential.credentials.id); const fileName = this.getCredentialsPath(sharedCredential.credentials.id);
const sanitizedCredential: ExportableCredential = { const sanitizedCredential: ExportableCredential = {

View file

@ -11,7 +11,7 @@ import * as Db from '@/Db';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { LoggerProxy, jsonParse } from 'n8n-workflow'; import { LoggerProxy, jsonParse } from 'n8n-workflow';
import { readFile as fsReadFile } from 'fs/promises'; import { readFile as fsReadFile } from 'fs/promises';
import { Credentials, UserSettings } from 'n8n-core'; import { Credentials, InstanceSettings } from 'n8n-core';
import type { IWorkflowToImport } from '@/Interfaces'; import type { IWorkflowToImport } from '@/Interfaces';
import type { ExportableCredential } from './types/exportableCredential'; import type { ExportableCredential } from './types/exportableCredential';
import type { Variables } from '@db/entities/Variables'; import type { Variables } from '@db/entities/Variables';
@ -41,9 +41,9 @@ export class SourceControlImportService {
private readonly variablesService: VariablesService, private readonly variablesService: VariablesService,
private readonly activeWorkflowRunner: ActiveWorkflowRunner, private readonly activeWorkflowRunner: ActiveWorkflowRunner,
private readonly tagRepository: TagRepository, private readonly tagRepository: TagRepository,
instanceSettings: InstanceSettings,
) { ) {
const userFolder = UserSettings.getUserN8nFolderPath(); this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER); this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
this.credentialExportFolder = path.join( this.credentialExportFolder = path.join(
this.gitFolder, this.gitFolder,
@ -81,69 +81,6 @@ export class SourceControlImportService {
return workflowOwnerRole; return workflowOwnerRole;
} }
private async importCredentialsFromFiles(
userId: string,
): Promise<Array<{ id: string; name: string; type: string }>> {
const credentialFiles = await glob('*.json', {
cwd: this.credentialExportFolder,
absolute: true,
});
const existingCredentials = await Db.collections.Credentials.find();
const ownerCredentialRole = await this.getCredentialOwnerRole();
const ownerGlobalRole = await this.getOwnerGlobalRole();
const encryptionKey = await UserSettings.getEncryptionKey();
let importCredentialsResult: Array<{ id: string; name: string; type: string }> = [];
importCredentialsResult = await Promise.all(
credentialFiles.map(async (file) => {
LoggerProxy.debug(`Importing credentials file ${file}`);
const credential = jsonParse<ExportableCredential>(
await fsReadFile(file, { encoding: 'utf8' }),
);
const existingCredential = existingCredentials.find(
(e) => e.id === credential.id && e.type === credential.type,
);
const sharedOwner = await Db.collections.SharedCredentials.findOne({
select: ['userId'],
where: {
credentialsId: credential.id,
roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]),
},
});
const { name, type, data, id, nodesAccess } = credential;
const newCredentialObject = new Credentials({ id, name }, type, []);
if (existingCredential?.data) {
newCredentialObject.data = existingCredential.data;
} else {
newCredentialObject.setData(data, encryptionKey);
}
newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || [];
LoggerProxy.debug(`Updating credential id ${newCredentialObject.id as string}`);
await Db.collections.Credentials.upsert(newCredentialObject, ['id']);
if (!sharedOwner) {
const newSharedCredential = new SharedCredentials();
newSharedCredential.credentialsId = newCredentialObject.id as string;
newSharedCredential.userId = userId;
newSharedCredential.roleId = ownerGlobalRole.id;
await Db.collections.SharedCredentials.upsert({ ...newSharedCredential }, [
'credentialsId',
'userId',
]);
}
return {
id: newCredentialObject.id as string,
name: newCredentialObject.name,
type: newCredentialObject.type,
};
}),
);
return importCredentialsResult.filter((e) => e !== undefined);
}
public async getRemoteVersionIdsFromFiles(): Promise<SourceControlWorkflowVersionId[]> { public async getRemoteVersionIdsFromFiles(): Promise<SourceControlWorkflowVersionId[]> {
const remoteWorkflowFiles = await glob('*.json', { const remoteWorkflowFiles = await glob('*.json', {
cwd: this.workflowExportFolder, cwd: this.workflowExportFolder,
@ -407,7 +344,6 @@ export class SourceControlImportService {
roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]), roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]),
}, },
}); });
const encryptionKey = await UserSettings.getEncryptionKey();
let importCredentialsResult: Array<{ id: string; name: string; type: string }> = []; let importCredentialsResult: Array<{ id: string; name: string; type: string }> = [];
importCredentialsResult = await Promise.all( importCredentialsResult = await Promise.all(
candidates.map(async (candidate) => { candidates.map(async (candidate) => {
@ -427,7 +363,7 @@ export class SourceControlImportService {
if (existingCredential?.data) { if (existingCredential?.data) {
newCredentialObject.data = existingCredential.data; newCredentialObject.data = existingCredential.data;
} else { } else {
newCredentialObject.setData(data, encryptionKey); newCredentialObject.setData(data);
} }
newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || []; newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || [];

View file

@ -9,7 +9,7 @@ import {
isSourceControlLicensed, isSourceControlLicensed,
sourceControlFoldersExistCheck, sourceControlFoldersExistCheck,
} from './sourceControlHelper.ee'; } from './sourceControlHelper.ee';
import { UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import { LoggerProxy, jsonParse } from 'n8n-workflow'; import { LoggerProxy, jsonParse } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { import {
@ -26,16 +26,15 @@ import config from '@/config';
export class SourceControlPreferencesService { export class SourceControlPreferencesService {
private _sourceControlPreferences: SourceControlPreferences = new SourceControlPreferences(); private _sourceControlPreferences: SourceControlPreferences = new SourceControlPreferences();
private sshKeyName: string; readonly sshKeyName: string;
private sshFolder: string; readonly sshFolder: string;
private gitFolder: string; readonly gitFolder: string;
constructor() { constructor(instanceSettings: InstanceSettings) {
const userFolder = UserSettings.getUserN8nFolderPath(); this.sshFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_SSH_FOLDER);
this.sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER); this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
this.sshKeyName = path.join(this.sshFolder, SOURCE_CONTROL_SSH_KEY_NAME); this.sshKeyName = path.join(this.sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
} }

View file

@ -18,7 +18,6 @@ import type {
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialsHelper } from '@/CredentialsHelper';
import { UserSettings } from 'n8n-core';
import { Agent as HTTPSAgent } from 'https'; import { Agent as HTTPSAgent } from 'https';
import config from '@/config'; import config from '@/config';
import { isLogStreamingEnabled } from '../MessageEventBus/MessageEventBusHelper'; import { isLogStreamingEnabled } from '../MessageEventBus/MessageEventBusHelper';
@ -26,6 +25,7 @@ import { eventMessageGenericDestinationTestEvent } from '../EventMessageClasses/
import { MessageEventBus } from '../MessageEventBus/MessageEventBus'; import { MessageEventBus } from '../MessageEventBus/MessageEventBus';
import type { MessageWithCallback } from '../MessageEventBus/MessageEventBus'; import type { MessageWithCallback } from '../MessageEventBus/MessageEventBus';
import * as SecretsHelpers from '@/ExternalSecrets/externalSecretsHelper.ee'; import * as SecretsHelpers from '@/ExternalSecrets/externalSecretsHelper.ee';
import Container from 'typedi';
export const isMessageEventBusDestinationWebhookOptions = ( export const isMessageEventBusDestinationWebhookOptions = (
candidate: unknown, candidate: unknown,
@ -135,13 +135,7 @@ export class MessageEventBusDestinationWebhook
} as AxiosRequestConfig; } as AxiosRequestConfig;
if (this.credentialsHelper === undefined) { if (this.credentialsHelper === undefined) {
let encryptionKey: string | undefined; this.credentialsHelper = Container.get(CredentialsHelper);
try {
encryptionKey = await UserSettings.getEncryptionKey();
} catch {}
if (encryptionKey) {
this.credentialsHelper = new CredentialsHelper(encryptionKey);
}
} }
const sendQuery = this.sendQuery; const sendQuery = this.sendQuery;

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { isEventMessageOptions } from '../EventMessageClasses/AbstractEventMessage'; import { isEventMessageOptions } from '../EventMessageClasses/AbstractEventMessage';
import { UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import path, { parse } from 'path'; import path, { parse } from 'path';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
import { createReadStream, existsSync, rmSync } from 'fs'; import { createReadStream, existsSync, rmSync } from 'fs';
@ -19,6 +19,7 @@ import {
} from '../EventMessageClasses/EventMessageConfirm'; } from '../EventMessageClasses/EventMessageConfirm';
import { once as eventOnce } from 'events'; import { once as eventOnce } from 'events';
import { inTest } from '@/constants'; import { inTest } from '@/constants';
import Container from 'typedi';
interface MessageEventBusLogWriterConstructorOptions { interface MessageEventBusLogWriterConstructorOptions {
logBaseName?: string; logBaseName?: string;
@ -66,7 +67,7 @@ export class MessageEventBusLogWriter {
MessageEventBusLogWriter.instance = new MessageEventBusLogWriter(); MessageEventBusLogWriter.instance = new MessageEventBusLogWriter();
MessageEventBusLogWriter.options = { MessageEventBusLogWriter.options = {
logFullBasePath: path.join( logFullBasePath: path.join(
options?.logBasePath ?? UserSettings.getUserN8nFolderPath(), options?.logBasePath ?? Container.get(InstanceSettings).n8nFolder,
options?.logBaseName ?? config.getEnv('eventBus.logWriter.logBaseName'), options?.logBaseName ?? config.getEnv('eventBus.logWriter.logBaseName'),
), ),
keepNumberOfFiles: keepNumberOfFiles:

View file

@ -1,6 +1,7 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { PostHog } from 'posthog-node'; import type { PostHog } from 'posthog-node';
import type { FeatureFlags, ITelemetryTrackProperties } from 'n8n-workflow'; import type { FeatureFlags, ITelemetryTrackProperties } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import type { PublicUser } from '@/Interfaces'; import type { PublicUser } from '@/Interfaces';
@ -8,10 +9,9 @@ import type { PublicUser } from '@/Interfaces';
export class PostHogClient { export class PostHogClient {
private postHog?: PostHog; private postHog?: PostHog;
private instanceId?: string; constructor(private readonly instanceSettings: InstanceSettings) {}
async init(instanceId: string) { async init() {
this.instanceId = instanceId;
const enabled = config.getEnv('diagnostics.enabled'); const enabled = config.getEnv('diagnostics.enabled');
if (!enabled) { if (!enabled) {
return; return;
@ -46,7 +46,7 @@ export class PostHogClient {
async getFeatureFlags(user: Pick<PublicUser, 'id' | 'createdAt'>): Promise<FeatureFlags> { async getFeatureFlags(user: Pick<PublicUser, 'id' | 'createdAt'>): Promise<FeatureFlags> {
if (!this.postHog) return {}; if (!this.postHog) return {};
const fullId = [this.instanceId, user.id].join('#'); const fullId = [this.instanceSettings.instanceId, user.id].join('#');
// cannot use local evaluation because that requires PostHog personal api key with org-wide // cannot use local evaluation because that requires PostHog personal api key with org-wide
// https://github.com/PostHog/posthog/issues/4849 // https://github.com/PostHog/posthog/issues/4849

View file

@ -6,7 +6,7 @@ import axios from 'axios';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow';
import type { PublicInstalledPackage } from 'n8n-workflow'; import type { PublicInstalledPackage } from 'n8n-workflow';
import { UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import type { PackageDirectoryLoader } from 'n8n-core'; import type { PackageDirectoryLoader } from 'n8n-core';
import { toError } from '@/utils'; import { toError } from '@/utils';
@ -47,6 +47,7 @@ export class CommunityPackagesService {
missingPackages: string[] = []; missingPackages: string[] = [];
constructor( constructor(
private readonly instanceSettings: InstanceSettings,
private readonly installedPackageRepository: InstalledPackagesRepository, private readonly installedPackageRepository: InstalledPackagesRepository,
private readonly loadNodesAndCredentials: LoadNodesAndCredentials, private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
) {} ) {}
@ -114,7 +115,7 @@ export class CommunityPackagesService {
} }
async executeNpmCommand(command: string, options?: { doNotHandleError?: boolean }) { async executeNpmCommand(command: string, options?: { doNotHandleError?: boolean }) {
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath(); const downloadFolder = this.instanceSettings.nodesDownloadDir;
const execOptions = { const execOptions = {
cwd: downloadFolder, cwd: downloadFolder,

View file

@ -10,6 +10,7 @@ import type {
INodeTypeBaseDescription, INodeTypeBaseDescription,
ITelemetrySettings, ITelemetrySettings,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core';
import { GENERATED_STATIC_DIR, LICENSE_FEATURES } from '@/constants'; import { GENERATED_STATIC_DIR, LICENSE_FEATURES } from '@/constants';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
@ -40,6 +41,7 @@ export class FrontendService {
private readonly credentialsOverwrites: CredentialsOverwrites, private readonly credentialsOverwrites: CredentialsOverwrites,
private readonly license: License, private readonly license: License,
private readonly mailer: UserManagementMailer, private readonly mailer: UserManagementMailer,
private readonly instanceSettings: InstanceSettings,
) { ) {
this.initSettings(); this.initSettings();
} }
@ -87,7 +89,7 @@ export class FrontendService {
endpoint: config.getEnv('versionNotifications.endpoint'), endpoint: config.getEnv('versionNotifications.endpoint'),
infoUrl: config.getEnv('versionNotifications.infoUrl'), infoUrl: config.getEnv('versionNotifications.infoUrl'),
}, },
instanceId: '', instanceId: this.instanceSettings.instanceId,
telemetry: telemetrySettings, telemetry: telemetrySettings,
posthog: { posthog: {
enabled: config.getEnv('diagnostics.enabled'), enabled: config.getEnv('diagnostics.enabled'),

View file

@ -3,7 +3,6 @@ import type { RedisServicePubSubPublisher } from '../../redis/RedisServicePubSub
export interface WorkerCommandReceivedHandlerOptions { export interface WorkerCommandReceivedHandlerOptions {
queueModeId: string; queueModeId: string;
instanceId: string;
redisPublisher: RedisServicePubSubPublisher; redisPublisher: RedisServicePubSubPublisher;
getRunningJobIds: () => string[]; getRunningJobIds: () => string[];
getRunningJobsSummary: () => WorkerJobStatusSummary[]; getRunningJobsSummary: () => WorkerJobStatusSummary[];

View file

@ -10,6 +10,7 @@ import { LicenseService } from '@/license/License.service';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee'; import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee';
import { InstanceSettings } from 'n8n-core';
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success'; type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
@ -30,8 +31,6 @@ interface IExecutionsBuffer {
@Service() @Service()
export class Telemetry { export class Telemetry {
private instanceId: string;
private rudderStack?: RudderStack; private rudderStack?: RudderStack;
private pulseIntervalReference: NodeJS.Timeout; private pulseIntervalReference: NodeJS.Timeout;
@ -41,12 +40,9 @@ export class Telemetry {
constructor( constructor(
private postHog: PostHogClient, private postHog: PostHogClient,
private license: License, private license: License,
private readonly instanceSettings: InstanceSettings,
) {} ) {}
setInstanceId(instanceId: string) {
this.instanceId = instanceId;
}
async init() { async init() {
const enabled = config.getEnv('diagnostics.enabled'); const enabled = config.getEnv('diagnostics.enabled');
if (enabled) { if (enabled) {
@ -172,15 +168,13 @@ export class Telemetry {
async identify(traits?: { async identify(traits?: {
[key: string]: string | number | boolean | object | undefined | null; [key: string]: string | number | boolean | object | undefined | null;
}): Promise<void> { }): Promise<void> {
const { instanceId } = this.instanceSettings;
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (this.rudderStack) { if (this.rudderStack) {
this.rudderStack.identify( this.rudderStack.identify(
{ {
userId: this.instanceId, userId: instanceId,
traits: { traits: { ...traits, instanceId },
...traits,
instanceId: this.instanceId,
},
}, },
resolve, resolve,
); );
@ -195,17 +189,18 @@ export class Telemetry {
properties: ITelemetryTrackProperties = {}, properties: ITelemetryTrackProperties = {},
{ withPostHog } = { withPostHog: false }, // whether to additionally track with PostHog { withPostHog } = { withPostHog: false }, // whether to additionally track with PostHog
): Promise<void> { ): Promise<void> {
const { instanceId } = this.instanceSettings;
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (this.rudderStack) { if (this.rudderStack) {
const { user_id } = properties; const { user_id } = properties;
const updatedProperties: ITelemetryTrackProperties = { const updatedProperties: ITelemetryTrackProperties = {
...properties, ...properties,
instance_id: this.instanceId, instance_id: instanceId,
version_cli: N8N_VERSION, version_cli: N8N_VERSION,
}; };
const payload = { const payload = {
userId: `${this.instanceId}${user_id ? `#${user_id}` : ''}`, userId: `${instanceId}${user_id ? `#${user_id}` : ''}`,
event: eventName, event: eventName,
properties: updatedProperties, properties: updatedProperties,
}; };

View file

@ -3,10 +3,9 @@ import { License } from '@/License';
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 type { ExternalSecretsSettings, SecretsProviderState } from '@/Interfaces'; import type { ExternalSecretsSettings, SecretsProviderState } from '@/Interfaces';
import { UserSettings } from 'n8n-core'; import { Cipher } from 'n8n-core';
import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { SettingsRepository } from '@/databases/repositories/settings.repository';
import Container from 'typedi'; import Container from 'typedi';
import { AES, enc } from 'crypto-js';
import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee'; import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee';
import { import {
DummyProvider, DummyProvider,
@ -17,7 +16,7 @@ import {
import config from '@/config'; import config from '@/config';
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
import type { IDataObject } from 'n8n-workflow'; import { jsonParse, type IDataObject } from 'n8n-workflow';
let authOwnerAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest; let authMemberAgent: SuperAgentTest;
@ -28,29 +27,24 @@ const licenseLike = utils.mockInstance(License, {
}); });
const mockProvidersInstance = new MockProviders(); const mockProvidersInstance = new MockProviders();
let providersMock: ExternalSecretsProviders = utils.mockInstance( utils.mockInstance(ExternalSecretsProviders, mockProvidersInstance);
ExternalSecretsProviders,
mockProvidersInstance,
);
const testServer = utils.setupTestServer({ endpointGroups: ['externalSecrets'] }); const testServer = utils.setupTestServer({ endpointGroups: ['externalSecrets'] });
const connectedDate = '2023-08-01T12:32:29.000Z'; const connectedDate = '2023-08-01T12:32:29.000Z';
async function setExternalSecretsSettings(settings: ExternalSecretsSettings) { async function setExternalSecretsSettings(settings: ExternalSecretsSettings) {
const encryptionKey = await UserSettings.getEncryptionKey();
return Container.get(SettingsRepository).saveEncryptedSecretsProviderSettings( return Container.get(SettingsRepository).saveEncryptedSecretsProviderSettings(
AES.encrypt(JSON.stringify(settings), encryptionKey).toString(), Container.get(Cipher).encrypt(settings),
); );
} }
async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | null> { async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | null> {
const encryptionKey = await UserSettings.getEncryptionKey();
const encSettings = await Container.get(SettingsRepository).getEncryptedSecretsProviderSettings(); const encSettings = await Container.get(SettingsRepository).getEncryptedSecretsProviderSettings();
if (encSettings === null) { if (encSettings === null) {
return null; return null;
} }
return JSON.parse(AES.decrypt(encSettings, encryptionKey).toString(enc.Utf8)); return jsonParse(Container.get(Cipher).decrypt(encSettings));
} }
const resetManager = async () => { const resetManager = async () => {
@ -61,6 +55,7 @@ const resetManager = async () => {
Container.get(SettingsRepository), Container.get(SettingsRepository),
licenseLike, licenseLike,
mockProvidersInstance, mockProvidersInstance,
Container.get(Cipher),
), ),
); );
@ -100,8 +95,6 @@ const getDummyProviderData = ({
}; };
beforeAll(async () => { beforeAll(async () => {
await utils.initEncryptionKey();
const owner = await testDb.createOwner(); const owner = await testDb.createOwner();
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
const member = await testDb.createUser(); const member = await testDb.createUser();

View file

@ -1,10 +1,8 @@
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { UserSettings } from 'n8n-core';
import type { IUser } from 'n8n-workflow'; import type { IUser } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { Credentials } from '@/requests'; import type { Credentials } from '@/requests';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
@ -304,21 +302,6 @@ describe('GET /credentials/:id', () => {
expect(response.body.data).toBeUndefined(); // owner's cred not returned expect(response.body.data).toBeUndefined(); // owner's cred not returned
}); });
test('should fail with missing encryption key', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authOwnerAgent
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
test('should return 404 if cred not found', async () => { test('should return 404 if cred not found', async () => {
const response = await authOwnerAgent.get('/credentials/789'); const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);

View file

@ -1,9 +1,7 @@
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import { UserSettings } from 'n8n-core';
import * as Db from '@/Db'; import * as Db from '@/Db';
import config from '@/config'; import config from '@/config';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import type { Credentials } from '@/requests'; import type { Credentials } from '@/requests';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
@ -130,17 +128,6 @@ describe('POST /credentials', () => {
} }
}); });
test('should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload());
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
test('should ignore ID in payload', async () => { test('should ignore ID in payload', async () => {
const firstResponse = await authOwnerAgent const firstResponse = await authOwnerAgent
.post('/credentials') .post('/credentials')
@ -385,17 +372,6 @@ describe('PATCH /credentials/:id', () => {
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
}); });
test('should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload());
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
}); });
describe('GET /credentials/new', () => { describe('GET /credentials/new', () => {
@ -504,21 +480,6 @@ describe('GET /credentials/:id', () => {
expect(response.body.data).toBeUndefined(); // owner's cred not returned expect(response.body.data).toBeUndefined(); // owner's cred not returned
}); });
test('should fail with missing encryption key', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authOwnerAgent
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
test('should return 404 if cred not found', async () => { test('should return 404 if cred not found', async () => {
const response = await authOwnerAgent.get('/credentials/789'); const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);

View file

@ -89,7 +89,6 @@ beforeAll(async () => {
mockedSyslog.createClient.mockImplementation(() => new syslog.Client()); mockedSyslog.createClient.mockImplementation(() => new syslog.Client());
await utils.initEncryptionKey();
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);

View file

@ -11,13 +11,15 @@ import type { User } from '@db/entities/User';
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants'; import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
import { LdapManager } from '@/Ldap/LdapManager.ee'; import { LdapManager } from '@/Ldap/LdapManager.ee';
import { LdapService } from '@/Ldap/LdapService.ee'; import { LdapService } from '@/Ldap/LdapService.ee';
import { encryptPassword, saveLdapSynchronization } from '@/Ldap/helpers'; import { saveLdapSynchronization } from '@/Ldap/helpers';
import type { LdapConfig } from '@/Ldap/types'; import type { LdapConfig } from '@/Ldap/types';
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { randomEmail, randomName, uniqueId } from './../shared/random'; import { randomEmail, randomName, uniqueId } 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 Container from 'typedi';
import { Cipher } from 'n8n-core';
jest.mock('@/telemetry'); jest.mock('@/telemetry');
@ -54,12 +56,10 @@ beforeAll(async () => {
owner = await testDb.createUser({ globalRole: globalOwnerRole }); owner = await testDb.createUser({ globalRole: globalOwnerRole });
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
defaultLdapConfig.bindingAdminPassword = await encryptPassword( defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(
defaultLdapConfig.bindingAdminPassword, defaultLdapConfig.bindingAdminPassword,
); );
await utils.initEncryptionKey();
await setCurrentAuthenticationMethod('email'); await setCurrentAuthenticationMethod('email');
}); });

View file

@ -35,7 +35,6 @@ const testServer = utils.setupTestServer({ endpointGroups: ['passwordReset'] });
const jwtService = Container.get(JwtService); const jwtService = Container.get(JwtService);
beforeAll(async () => { beforeAll(async () => {
await utils.initEncryptionKey();
globalOwnerRole = await testDb.getGlobalOwnerRole(); globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole(); globalMemberRole = await testDb.getGlobalMemberRole();
}); });

View file

@ -1,9 +1,7 @@
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import { UserSettings } from 'n8n-core';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { randomApiKey, randomName, randomString } from '../shared/random'; import { randomApiKey, randomName, randomString } from '../shared/random';
import * as utils from '../shared/utils/'; import * as utils from '../shared/utils/';
@ -22,9 +20,6 @@ let saveCredential: SaveCredentialFunction;
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] }); const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
beforeAll(async () => { beforeAll(async () => {
// TODO: mock encryption key
await utils.initEncryptionKey();
const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] = const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] =
await testDb.getAllRoles(); await testDb.getAllRoles();
@ -87,17 +82,6 @@ describe('POST /credentials', () => {
expect(response.statusCode === 400 || response.statusCode === 415).toBe(true); expect(response.statusCode === 400 || response.statusCode === 415).toBe(true);
} }
}); });
test('should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authOwnerAgent.post('/credentials').send(credentialPayload());
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
}); });
describe('DELETE /credentials/:id', () => { describe('DELETE /credentials/:id', () => {

View file

@ -35,7 +35,6 @@ beforeAll(async () => {
apiKey: randomApiKey(), apiKey: randomApiKey(),
}); });
await utils.initEncryptionKey();
await utils.initNodeTypes(); await utils.initNodeTypes();
workflowRunner = await utils.initActiveWorkflowRunner(); workflowRunner = await utils.initActiveWorkflowRunner();
}); });

View file

@ -1,4 +1,3 @@
import { UserSettings } from 'n8n-core';
import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm'; import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm';
import { DataSource as Connection } from 'typeorm'; import { DataSource as Connection } from 'typeorm';
import { Container } from 'typedi'; import { Container } from 'typedi';
@ -213,8 +212,6 @@ export async function createLdapUser(attributes: Partial<User>, ldapId: string):
export async function createUserWithMfaEnabled( export async function createUserWithMfaEnabled(
data: { numberOfRecoveryCodes: number } = { numberOfRecoveryCodes: 10 }, data: { numberOfRecoveryCodes: number } = { numberOfRecoveryCodes: 10 },
) { ) {
const encryptionKey = await UserSettings.getEncryptionKey();
const email = randomEmail(); const email = randomEmail();
const password = randomPassword(); const password = randomPassword();
@ -222,7 +219,7 @@ export async function createUserWithMfaEnabled(
const secret = toptService.generateSecret(); const secret = toptService.generateSecret();
const mfaService = new MfaService(Db.collections.User, toptService, encryptionKey); const mfaService = Container.get(MfaService);
const recoveryCodes = mfaService.generateRecoveryCodes(data.numberOfRecoveryCodes); const recoveryCodes = mfaService.generateRecoveryCodes(data.numberOfRecoveryCodes);
@ -687,12 +684,10 @@ const getDBOptions = (type: TestDBType, name: string) => ({
// ---------------------------------- // ----------------------------------
async function encryptCredentialData(credential: CredentialsEntity) { async function encryptCredentialData(credential: CredentialsEntity) {
const encryptionKey = await UserSettings.getEncryptionKey();
const coreCredential = createCredentialsFromCredentialsEntity(credential, true); const coreCredential = createCredentialsFromCredentialsEntity(credential, true);
// @ts-ignore // @ts-ignore
coreCredential.setData(credential.data, encryptionKey); coreCredential.setData(credential.data);
return coreCredential.getDataToSave() as ICredentialsDb; return coreCredential.getDataToSave() as ICredentialsDb;
} }

View file

@ -1,7 +1,5 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { randomBytes } from 'crypto'; import { BinaryDataService } from 'n8n-core';
import { existsSync } from 'fs';
import { BinaryDataService, UserSettings } from 'n8n-core';
import type { INode } from 'n8n-workflow'; import type { INode } from 'n8n-workflow';
import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.credentials'; import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.credentials';
import { Ftp } from 'n8n-nodes-base/credentials/Ftp.credentials'; import { Ftp } from 'n8n-nodes-base/credentials/Ftp.credentials';
@ -84,19 +82,6 @@ export async function initBinaryDataService(mode: 'default' | 'filesystem' = 'de
Container.set(BinaryDataService, binaryDataService); Container.set(BinaryDataService, binaryDataService);
} }
/**
* Initialize a user settings config file if non-existent.
*/
// TODO: this should be mocked
export async function initEncryptionKey() {
const settingsPath = UserSettings.getUserSettingsPath();
if (!existsSync(settingsPath)) {
const userSettings = { encryptionKey: randomBytes(24).toString('base64') };
await UserSettings.writeUserSettings(userSettings, settingsPath);
}
}
/** /**
* Extract the value (token) of the auth cookie in a response. * Extract the value (token) of the auth cookie in a response.
*/ */

View file

@ -7,7 +7,6 @@ import request from 'supertest';
import { URL } from 'url'; import { URL } from 'url';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { workflowsController } from '@/workflows/workflows.controller'; import { workflowsController } from '@/workflows/workflows.controller';
@ -50,8 +49,6 @@ import type { EndpointGroup, SetupProps, TestServer } from '../types';
import { mockInstance } from './mocking'; import { mockInstance } from './mocking';
import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee'; import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
import { MfaService } from '@/Mfa/mfa.service'; import { MfaService } from '@/Mfa/mfa.service';
import { TOTPService } from '@/Mfa/totp.service';
import { UserSettings } from 'n8n-core';
import { MetricsService } from '@/services/metrics.service'; import { MetricsService } from '@/services/metrics.service';
import { import {
SettingsRepository, SettingsRepository,
@ -200,12 +197,10 @@ export const setupTestServer = ({
} }
if (functionEndpoints.length) { if (functionEndpoints.length) {
const encryptionKey = await UserSettings.getEncryptionKey();
const repositories = Db.collections;
const externalHooks = Container.get(ExternalHooks); const externalHooks = Container.get(ExternalHooks);
const internalHooks = Container.get(InternalHooks); const internalHooks = Container.get(InternalHooks);
const mailer = Container.get(UserManagementMailer); const mailer = Container.get(UserManagementMailer);
const mfaService = new MfaService(repositories.User, new TOTPService(), encryptionKey); const mfaService = Container.get(MfaService);
const userService = Container.get(UserService); const userService = Container.get(UserService);
for (const group of functionEndpoints) { for (const group of functionEndpoints) {

View file

@ -16,7 +16,6 @@ const licenseLike = {
const testServer = utils.setupTestServer({ endpointGroups: ['variables'] }); const testServer = utils.setupTestServer({ endpointGroups: ['variables'] });
beforeAll(async () => { beforeAll(async () => {
await utils.initEncryptionKey();
utils.mockInstance(License, licenseLike); utils.mockInstance(License, licenseLike);
const owner = await testDb.createOwner(); const owner = await testDb.createOwner();

View file

@ -0,0 +1,16 @@
import { tmpdir } from 'os';
import { join } from 'path';
import { mkdirSync, mkdtempSync, writeFileSync } from 'fs';
const baseDir = join(tmpdir(), 'n8n-tests/');
mkdirSync(baseDir, { recursive: true });
const testDir = mkdtempSync(baseDir);
mkdirSync(join(testDir, '.n8n'));
process.env.N8N_USER_FOLDER = testDir;
writeFileSync(
join(testDir, '.n8n/config'),
JSON.stringify({ encryptionKey: 'testkey', instanceId: '123' }),
'utf-8',
);

View file

@ -12,6 +12,7 @@ import { CredentialsHelper } from '@/CredentialsHelper';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils'; import { mockInstance } from '../integration/shared/utils';
import Container from 'typedi';
describe('CredentialsHelper', () => { describe('CredentialsHelper', () => {
const TEST_ENCRYPTION_KEY = 'test'; const TEST_ENCRYPTION_KEY = 'test';
@ -277,7 +278,7 @@ describe('CredentialsHelper', () => {
}, },
}; };
const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY); const credentialsHelper = Container.get(CredentialsHelper);
const result = await credentialsHelper.authenticate( const result = await credentialsHelper.authenticate(
testData.input.credentials, testData.input.credentials,

View file

@ -1,11 +1,11 @@
import type { SettingsRepository } from '@/databases/repositories'; import { Container } from 'typedi';
import { Cipher } from 'n8n-core';
import { SettingsRepository } from '@/databases/repositories';
import type { ExternalSecretsSettings } from '@/Interfaces'; import type { ExternalSecretsSettings } from '@/Interfaces';
import { License } from '@/License'; import { License } from '@/License';
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee'; import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee'; import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee';
import { mock } from 'jest-mock-extended'; import { InternalHooks } from '@/InternalHooks';
import { UserSettings } from 'n8n-core';
import Container from 'typedi';
import { mockInstance } from '../../integration/shared/utils'; import { mockInstance } from '../../integration/shared/utils';
import { import {
DummyProvider, DummyProvider,
@ -13,56 +13,42 @@ import {
FailedProvider, FailedProvider,
MockProviders, MockProviders,
} from '../../shared/ExternalSecrets/utils'; } from '../../shared/ExternalSecrets/utils';
import { AES, enc } from 'crypto-js';
import { InternalHooks } from '@/InternalHooks';
const connectedDate = '2023-08-01T12:32:29.000Z';
const encryptionKey = 'testkey';
let settings: string | null = null;
const mockProvidersInstance = new MockProviders();
const settingsRepo = mock<SettingsRepository>({
async getEncryptedSecretsProviderSettings() {
return settings;
},
async saveEncryptedSecretsProviderSettings(data) {
settings = data;
},
});
let licenseMock: License;
let providersMock: ExternalSecretsProviders;
let manager: ExternalSecretsManager | undefined;
const createMockSettings = (settings: ExternalSecretsSettings): string => {
return AES.encrypt(JSON.stringify(settings), encryptionKey).toString();
};
const decryptSettings = (settings: string) => {
return JSON.parse(AES.decrypt(settings ?? '', encryptionKey).toString(enc.Utf8));
};
describe('External Secrets Manager', () => { describe('External Secrets Manager', () => {
const connectedDate = '2023-08-01T12:32:29.000Z';
let settings: string | null = null;
const mockProvidersInstance = new MockProviders();
const license = mockInstance(License);
const settingsRepo = mockInstance(SettingsRepository);
mockInstance(InternalHooks);
const cipher = Container.get(Cipher);
let providersMock: ExternalSecretsProviders;
let manager: ExternalSecretsManager;
const createMockSettings = (settings: ExternalSecretsSettings): string => {
return cipher.encrypt(settings);
};
const decryptSettings = (settings: string) => {
return JSON.parse(cipher.decrypt(settings));
};
beforeAll(() => { beforeAll(() => {
jest
.spyOn(UserSettings, 'getEncryptionKey')
.mockReturnValue(new Promise((resolve) => resolve(encryptionKey)));
providersMock = mockInstance(ExternalSecretsProviders, mockProvidersInstance); providersMock = mockInstance(ExternalSecretsProviders, mockProvidersInstance);
licenseMock = mockInstance(License, { settings = createMockSettings({
isExternalSecretsEnabled() { dummy: { connected: true, connectedAt: new Date(connectedDate), settings: {} },
return true;
},
}); });
mockInstance(InternalHooks);
}); });
beforeEach(() => { beforeEach(() => {
mockProvidersInstance.setProviders({ mockProvidersInstance.setProviders({
dummy: DummyProvider, dummy: DummyProvider,
}); });
settings = createMockSettings({ license.isExternalSecretsEnabled.mockReturnValue(true);
dummy: { connected: true, connectedAt: new Date(connectedDate), settings: {} }, settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
}); manager = new ExternalSecretsManager(settingsRepo, license, providersMock, cipher);
Container.remove(ExternalSecretsManager);
}); });
afterEach(() => { afterEach(() => {
@ -71,8 +57,6 @@ describe('External Secrets Manager', () => {
}); });
test('should get secret', async () => { test('should get secret', async () => {
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init(); await manager.init();
expect(manager.getSecret('dummy', 'test1')).toBe('value1'); expect(manager.getSecret('dummy', 'test1')).toBe('value1');
@ -82,8 +66,6 @@ describe('External Secrets Manager', () => {
mockProvidersInstance.setProviders({ mockProvidersInstance.setProviders({
dummy: ErrorProvider, dummy: ErrorProvider,
}); });
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
expect(async () => manager!.init()).not.toThrow(); expect(async () => manager!.init()).not.toThrow();
}); });
@ -91,16 +73,12 @@ describe('External Secrets Manager', () => {
mockProvidersInstance.setProviders({ mockProvidersInstance.setProviders({
dummy: ErrorProvider, dummy: ErrorProvider,
}); });
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init(); await manager.init();
expect(() => manager!.shutdown()).not.toThrow(); expect(() => manager!.shutdown()).not.toThrow();
manager = undefined;
}); });
test('should save provider settings', async () => { test('should save provider settings', async () => {
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
const settingsSpy = jest.spyOn(settingsRepo, 'saveEncryptedSecretsProviderSettings'); const settingsSpy = jest.spyOn(settingsRepo, 'saveEncryptedSecretsProviderSettings');
await manager.init(); await manager.init();
@ -122,8 +100,6 @@ describe('External Secrets Manager', () => {
test('should call provider update functions on a timer', async () => { test('should call provider update functions on a timer', async () => {
jest.useFakeTimers(); jest.useFakeTimers();
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init(); await manager.init();
const updateSpy = jest.spyOn(manager.getProvider('dummy')!, 'update'); const updateSpy = jest.spyOn(manager.getProvider('dummy')!, 'update');
@ -138,15 +114,7 @@ describe('External Secrets Manager', () => {
test('should not call provider update functions if the not licensed', async () => { test('should not call provider update functions if the not licensed', async () => {
jest.useFakeTimers(); jest.useFakeTimers();
manager = new ExternalSecretsManager( license.isExternalSecretsEnabled.mockReturnValue(false);
settingsRepo,
mock<License>({
isExternalSecretsEnabled() {
return false;
},
}),
providersMock,
);
await manager.init(); await manager.init();
@ -165,7 +133,6 @@ describe('External Secrets Manager', () => {
mockProvidersInstance.setProviders({ mockProvidersInstance.setProviders({
dummy: FailedProvider, dummy: FailedProvider,
}); });
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init(); await manager.init();
@ -179,8 +146,6 @@ describe('External Secrets Manager', () => {
}); });
test('should reinitialize a provider when save provider settings', async () => { test('should reinitialize a provider when save provider settings', async () => {
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init(); await manager.init();
const dummyInitSpy = jest.spyOn(DummyProvider.prototype, 'init'); const dummyInitSpy = jest.spyOn(DummyProvider.prototype, 'init');

View file

@ -1,7 +1,9 @@
import { LicenseManager } from '@n8n_io/license-sdk'; import { LicenseManager } from '@n8n_io/license-sdk';
import { InstanceSettings } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import { License } from '@/License'; import { License } from '@/License';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import { mockInstance } from '../integration/shared/utils';
jest.mock('@n8n_io/license-sdk'); jest.mock('@n8n_io/license-sdk');
@ -21,10 +23,11 @@ describe('License', () => {
}); });
let license: License; let license: License;
const instanceSettings = mockInstance(InstanceSettings, { instanceId: MOCK_INSTANCE_ID });
beforeEach(async () => { beforeEach(async () => {
license = new License(); license = new License(instanceSettings);
await license.init(MOCK_INSTANCE_ID); await license.init();
}); });
test('initializes license manager', async () => { test('initializes license manager', async () => {
@ -45,8 +48,8 @@ describe('License', () => {
}); });
test('initializes license manager for worker', async () => { test('initializes license manager for worker', async () => {
license = new License(); license = new License(instanceSettings);
await license.init(MOCK_INSTANCE_ID, 'worker'); await license.init('worker');
expect(LicenseManager).toHaveBeenCalledWith({ expect(LicenseManager).toHaveBeenCalledWith({
autoRenewEnabled: false, autoRenewEnabled: false,
autoRenewOffset: MOCK_RENEW_OFFSET, autoRenewOffset: MOCK_RENEW_OFFSET,

View file

@ -1,6 +1,8 @@
import { PostHog } from 'posthog-node'; import { PostHog } from 'posthog-node';
import { InstanceSettings } from 'n8n-core';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import config from '@/config'; import config from '@/config';
import { mockInstance } from '../integration/shared/utils';
jest.mock('posthog-node'); jest.mock('posthog-node');
@ -10,6 +12,8 @@ describe('PostHog', () => {
const apiKey = 'api-key'; const apiKey = 'api-key';
const apiHost = 'api-host'; const apiHost = 'api-host';
const instanceSettings = mockInstance(InstanceSettings, { instanceId });
beforeAll(() => { beforeAll(() => {
config.set('diagnostics.config.posthog.apiKey', apiKey); config.set('diagnostics.config.posthog.apiKey', apiKey);
config.set('diagnostics.config.posthog.apiHost', apiHost); config.set('diagnostics.config.posthog.apiHost', apiHost);
@ -21,8 +25,8 @@ describe('PostHog', () => {
}); });
it('inits PostHog correctly', async () => { it('inits PostHog correctly', async () => {
const ph = new PostHogClient(); const ph = new PostHogClient(instanceSettings);
await ph.init(instanceId); await ph.init();
expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, { host: apiHost }); expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, { host: apiHost });
}); });
@ -30,8 +34,8 @@ describe('PostHog', () => {
it('does not initialize or track if diagnostics are not enabled', async () => { it('does not initialize or track if diagnostics are not enabled', async () => {
config.set('diagnostics.enabled', false); config.set('diagnostics.enabled', false);
const ph = new PostHogClient(); const ph = new PostHogClient(instanceSettings);
await ph.init(instanceId); await ph.init();
ph.track({ ph.track({
userId: 'test', userId: 'test',
@ -50,8 +54,8 @@ describe('PostHog', () => {
test: true, test: true,
}; };
const ph = new PostHogClient(); const ph = new PostHogClient(instanceSettings);
await ph.init(instanceId); await ph.init();
ph.track({ ph.track({
userId, userId,
@ -70,8 +74,8 @@ describe('PostHog', () => {
it('gets feature flags', async () => { it('gets feature flags', async () => {
const createdAt = new Date(); const createdAt = new Date();
const ph = new PostHogClient(); const ph = new PostHogClient(instanceSettings);
await ph.init(instanceId); await ph.init();
await ph.getFeatureFlags({ await ph.getFeatureFlags({
id: userId, id: userId,

View file

@ -9,12 +9,11 @@ import {
} from '@/environments/sourceControl/sourceControlHelper.ee'; } from '@/environments/sourceControl/sourceControlHelper.ee';
import { License } from '@/License'; import { License } from '@/License';
import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee'; import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee';
import { UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import path from 'path'; import path from 'path';
import { import {
SOURCE_CONTROL_SSH_FOLDER, SOURCE_CONTROL_SSH_FOLDER,
SOURCE_CONTROL_GIT_FOLDER, SOURCE_CONTROL_GIT_FOLDER,
SOURCE_CONTROL_SSH_KEY_NAME,
} from '@/environments/sourceControl/constants'; } from '@/environments/sourceControl/constants';
import { LoggerProxy } from 'n8n-workflow'; import { LoggerProxy } from 'n8n-workflow';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
@ -184,10 +183,9 @@ describe('Source Control', () => {
}); });
it('should check for git and ssh folders and create them if required', async () => { it('should check for git and ssh folders and create them if required', async () => {
const userFolder = UserSettings.getUserN8nFolderPath(); const { n8nFolder } = Container.get(InstanceSettings);
const sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER); const sshFolder = path.join(n8nFolder, SOURCE_CONTROL_SSH_FOLDER);
const gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER); const gitFolder = path.join(n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
const sshKeyName = path.join(sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
let hasThrown = false; let hasThrown = false;
try { try {
accessSync(sshFolder, fsConstants.F_OK); accessSync(sshFolder, fsConstants.F_OK);

View file

@ -4,6 +4,8 @@ import config from '@/config';
import { flushPromises } from './Helpers'; import { flushPromises } from './Helpers';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { mockInstance } from '../integration/shared/utils';
import { InstanceSettings } from 'n8n-core';
jest.unmock('@/telemetry'); jest.unmock('@/telemetry');
jest.mock('@/license/License.service', () => { jest.mock('@/license/License.service', () => {
@ -28,6 +30,7 @@ describe('Telemetry', () => {
let telemetry: Telemetry; let telemetry: Telemetry;
const instanceId = 'Telemetry unit test'; const instanceId = 'Telemetry unit test';
const testDateTime = new Date('2022-01-01 00:00:00'); const testDateTime = new Date('2022-01-01 00:00:00');
const instanceSettings = mockInstance(InstanceSettings, { instanceId });
beforeAll(() => { beforeAll(() => {
startPulseSpy = jest startPulseSpy = jest
@ -49,11 +52,10 @@ describe('Telemetry', () => {
beforeEach(async () => { beforeEach(async () => {
spyTrack.mockClear(); spyTrack.mockClear();
const postHog = new PostHogClient(); const postHog = new PostHogClient(instanceSettings);
await postHog.init(instanceId); await postHog.init();
telemetry = new Telemetry(postHog, mock()); telemetry = new Telemetry(postHog, mock(), instanceSettings);
telemetry.setInstanceId(instanceId);
(telemetry as any).rudderStack = mockRudderStack; (telemetry as any).rudderStack = mockRudderStack;
}); });

View file

@ -37,7 +37,7 @@
"@types/aws4": "^1.5.1", "@types/aws4": "^1.5.1",
"@types/concat-stream": "^2.0.0", "@types/concat-stream": "^2.0.0",
"@types/cron": "~1.7.1", "@types/cron": "~1.7.1",
"@types/crypto-js": "^4.0.1", "@types/crypto-js": "^4.1.3",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/lodash": "^4.14.195", "@types/lodash": "^4.14.195",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
@ -54,7 +54,7 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"concat-stream": "^2.0.0", "concat-stream": "^2.0.0",
"cron": "~1.7.2", "cron": "~1.7.2",
"crypto-js": "~4.1.1", "crypto-js": "^4.1.1",
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"file-type": "^16.5.4", "file-type": "^16.5.4",
"flatted": "^3.2.4", "flatted": "^3.2.4",

View file

@ -0,0 +1,19 @@
import { Service } from 'typedi';
import { AES, enc } from 'crypto-js';
import { InstanceSettings } from './InstanceSettings';
@Service()
export class Cipher {
constructor(private readonly instanceSettings: InstanceSettings) {}
encrypt(data: string | object) {
return AES.encrypt(
typeof data === 'string' ? data : JSON.stringify(data),
this.instanceSettings.encryptionKey,
).toString();
}
decrypt(data: string) {
return AES.decrypt(data, this.instanceSettings.encryptionKey).toString(enc.Utf8);
}
}

View file

@ -1,17 +1,6 @@
export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS'; export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS';
export const DOWNLOADED_NODES_SUBDIRECTORY = 'nodes';
export const ENCRYPTION_KEY_ENV_OVERWRITE = 'N8N_ENCRYPTION_KEY';
export const EXTENSIONS_SUBDIRECTORY = 'custom';
export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER';
export const USER_SETTINGS_FILE_NAME = 'config';
export const USER_SETTINGS_SUBFOLDER = '.n8n';
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__'; export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__';
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__'; export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
export const RESPONSE_ERROR_MESSAGES = {
NO_ENCRYPTION_KEY: 'Encryption key is missing or was not set',
};
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';

View file

@ -1,13 +1,11 @@
import type { import { Container } from 'typedi';
CredentialInformation, import type { ICredentialDataDecryptedObject, ICredentialsEncrypted } from 'n8n-workflow';
ICredentialDataDecryptedObject, import { ICredentials, jsonParse } from 'n8n-workflow';
ICredentialsEncrypted, import { Cipher } from './Cipher';
} from 'n8n-workflow';
import { ICredentials } from 'n8n-workflow';
import { AES, enc } from 'crypto-js';
export class Credentials extends ICredentials { export class Credentials extends ICredentials {
private readonly cipher = Container.get(Cipher);
/** /**
* Returns if the given nodeType has access to data * Returns if the given nodeType has access to data
*/ */
@ -24,30 +22,14 @@ export class Credentials extends ICredentials {
/** /**
* Sets new credential object * Sets new credential object
*/ */
setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void { setData(data: ICredentialDataDecryptedObject): void {
this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString(); this.data = this.cipher.encrypt(data);
}
/**
* Sets new credentials for given key
*/
setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void {
let fullData;
try {
fullData = this.getData(encryptionKey);
} catch (e) {
fullData = {};
}
fullData[key] = data;
return this.setData(fullData, encryptionKey);
} }
/** /**
* Returns the decrypted credential object * Returns the decrypted credential object
*/ */
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject { getData(nodeType?: string): ICredentialDataDecryptedObject {
if (nodeType && !this.hasNodeAccess(nodeType)) { if (nodeType && !this.hasNodeAccess(nodeType)) {
throw new Error( throw new Error(
`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`, `The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`,
@ -58,11 +40,10 @@ export class Credentials extends ICredentials {
throw new Error('No data is set so nothing can be returned.'); throw new Error('No data is set so nothing can be returned.');
} }
const decryptedData = AES.decrypt(this.data, encryptionKey); const decryptedData = this.cipher.decrypt(this.data);
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return return jsonParse(decryptedData);
return JSON.parse(decryptedData.toString(enc.Utf8));
} catch (e) { } catch (e) {
throw new Error( throw new Error(
'Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.', 'Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
@ -70,23 +51,6 @@ export class Credentials extends ICredentials {
} }
} }
/**
* Returns the decrypted credentials for given key
*/
getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation {
const fullData = this.getData(encryptionKey, nodeType);
if (fullData === null) {
throw new Error('No data was set.');
}
if (!fullData.hasOwnProperty(key)) {
throw new Error(`No data for key "${key}" exists.`);
}
return fullData[key];
}
/** /**
* Returns the encrypted credentials to be saved * Returns the encrypted credentials to be saved
*/ */

View file

@ -0,0 +1,86 @@
import path from 'path';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { createHash, randomBytes } from 'crypto';
import { Service } from 'typedi';
import { jsonParse } from 'n8n-workflow';
interface ReadOnlySettings {
encryptionKey: string;
instanceId: string;
}
interface WritableSettings {
tunnelSubdomain?: string;
}
type Settings = ReadOnlySettings & WritableSettings;
@Service()
export class InstanceSettings {
readonly userHome = this.getUserHome();
/** The path to the n8n folder in which all n8n related data gets saved */
readonly n8nFolder = path.join(this.userHome, '.n8n');
/** The path to the folder containing custom nodes and credentials */
readonly customExtensionDir = path.join(this.n8nFolder, 'custom');
/** The path to the folder containing installed nodes (like community nodes) */
readonly nodesDownloadDir = path.join(this.n8nFolder, 'nodes');
private readonly settingsFile = path.join(this.n8nFolder, 'config');
private settings = this.loadOrCreate();
get encryptionKey() {
return this.settings.encryptionKey;
}
get instanceId() {
return this.settings.instanceId;
}
get tunnelSubdomain() {
return this.settings.tunnelSubdomain;
}
update(newSettings: WritableSettings) {
this.save({ ...this.settings, ...newSettings });
}
/**
* The home folder path of the user.
* If none can be found it falls back to the current working directory
*/
private getUserHome() {
const homeVarName = process.platform === 'win32' ? 'USERPROFILE' : 'HOME';
return process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd();
}
private loadOrCreate(): Settings {
const { settingsFile } = this;
if (existsSync(settingsFile)) {
const content = readFileSync(settingsFile, 'utf8');
return jsonParse(content, {
errorMessage: `Error parsing n8n-config file "${settingsFile}". It does not seem to be valid JSON.`,
});
}
// If file doesn't exist, create new settings
const encryptionKey = process.env.N8N_ENCRYPTION_KEY ?? randomBytes(24).toString('base64');
const instanceId = createHash('sha256')
.update(encryptionKey.slice(Math.round(encryptionKey.length / 2)))
.digest('hex');
const settings = { encryptionKey, instanceId };
mkdirSync(path.dirname(settingsFile));
this.save(settings);
console.log(`UserSettings were generated and saved to: ${settingsFile}`);
return settings;
}
private save(settings: Settings) {
this.settings = settings;
writeFileSync(this.settingsFile, JSON.stringify(settings, null, '\t'), 'utf-8');
}
}

View file

@ -15,12 +15,6 @@ export interface IResponseError extends Error {
statusCode?: number; statusCode?: number;
} }
export interface IUserSettings {
encryptionKey?: string;
tunnelSubdomain?: string;
instanceId?: string;
}
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
errorWorkflow?: string; errorWorkflow?: string;
timezone?: string; timezone?: string;

View file

@ -3,14 +3,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/no-shadow */
import type { import type {
ClientOAuth2Options, ClientOAuth2Options,
ClientOAuth2RequestObject, ClientOAuth2RequestObject,
@ -143,9 +140,9 @@ import {
setWorkflowExecutionMetadata, setWorkflowExecutionMetadata,
} from './WorkflowExecutionMetadata'; } from './WorkflowExecutionMetadata';
import { getSecretsProxy } from './Secrets'; import { getSecretsProxy } from './Secrets';
import { getUserN8nFolderPath, getInstanceId } from './UserSettings';
import Container from 'typedi'; import Container from 'typedi';
import type { BinaryData } from './BinaryData/types'; import type { BinaryData } from './BinaryData/types';
import { InstanceSettings } from './InstanceSettings';
axios.defaults.timeout = 300000; axios.defaults.timeout = 300000;
// Prevent axios from adding x-form-www-urlencoded headers by default // Prevent axios from adding x-form-www-urlencoded headers by default
@ -2510,7 +2507,7 @@ const getCommonWorkflowFunctions = (
getRestApiUrl: () => additionalData.restApiUrl, getRestApiUrl: () => additionalData.restApiUrl,
getInstanceBaseUrl: () => additionalData.instanceBaseUrl, getInstanceBaseUrl: () => additionalData.instanceBaseUrl,
getInstanceId: async () => getInstanceId(), getInstanceId: () => Container.get(InstanceSettings).instanceId,
getTimezone: () => getTimezone(workflow, additionalData), getTimezone: () => getTimezone(workflow, additionalData),
prepareOutputData: async (outputData) => [outputData], prepareOutputData: async (outputData) => [outputData],
@ -2600,7 +2597,6 @@ const getAllowedPaths = () => {
function isFilePathBlocked(filePath: string): boolean { function isFilePathBlocked(filePath: string): boolean {
const allowedPaths = getAllowedPaths(); const allowedPaths = getAllowedPaths();
const resolvedFilePath = path.resolve(filePath); const resolvedFilePath = path.resolve(filePath);
const userFolder = getUserN8nFolderPath();
const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false'; const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false';
//if allowed paths are defined, allow access only to those paths //if allowed paths are defined, allow access only to those paths
@ -2616,7 +2612,8 @@ function isFilePathBlocked(filePath: string): boolean {
//restrict access to .n8n folder and other .env config related paths //restrict access to .n8n folder and other .env config related paths
if (blockFileAccessToN8nFiles) { if (blockFileAccessToN8nFiles) {
const restrictedPaths: string[] = [userFolder]; const { n8nFolder } = Container.get(InstanceSettings);
const restrictedPaths = [n8nFolder];
if (process.env[CONFIG_FILES]) { if (process.env[CONFIG_FILES]) {
restrictedPaths.push(...process.env[CONFIG_FILES].split(',')); restrictedPaths.push(...process.env[CONFIG_FILES].split(','));
@ -2674,7 +2671,7 @@ const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunctions =>
}, },
getStoragePath() { getStoragePath() {
return path.join(getUserN8nFolderPath(), `storage/${node.type}`); return path.join(Container.get(InstanceSettings).n8nFolder, `storage/${node.type}`);
}, },
async writeContentToFile(filePath, content, flag) { async writeContentToFile(filePath, content, flag) {

View file

@ -1,272 +0,0 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import fs from 'fs';
import path from 'path';
import { createHash, randomBytes } from 'crypto';
import { promisify } from 'util';
import { deepCopy } from 'n8n-workflow';
import {
ENCRYPTION_KEY_ENV_OVERWRITE,
EXTENSIONS_SUBDIRECTORY,
DOWNLOADED_NODES_SUBDIRECTORY,
RESPONSE_ERROR_MESSAGES,
USER_FOLDER_ENV_OVERWRITE,
USER_SETTINGS_FILE_NAME,
USER_SETTINGS_SUBFOLDER,
} from './Constants';
import type { IUserSettings } from './Interfaces';
const fsAccess = promisify(fs.access);
const fsReadFile = promisify(fs.readFile);
const fsMkdir = promisify(fs.mkdir);
const fsWriteFile = promisify(fs.writeFile);
let settingsCache: IUserSettings | undefined;
/**
* Creates the user settings if they do not exist yet
*
*/
export async function prepareUserSettings(): Promise<IUserSettings> {
const settingsPath = getUserSettingsPath();
let userSettings = await getUserSettings(settingsPath);
if (userSettings !== undefined) {
// Settings already exist, check if they contain the encryptionKey
if (userSettings.encryptionKey !== undefined) {
// Key already exists
if (userSettings.instanceId === undefined) {
userSettings.instanceId = await generateInstanceId(userSettings.encryptionKey);
settingsCache = userSettings;
}
return userSettings;
}
} else {
userSettings = {};
}
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
// Use the encryption key which got set via environment
userSettings.encryptionKey = process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
} else {
// Generate a new encryption key
userSettings.encryptionKey = randomBytes(24).toString('base64');
}
userSettings.instanceId = await generateInstanceId(userSettings.encryptionKey);
console.log(`UserSettings were generated and saved to: ${settingsPath}`);
return writeUserSettings(userSettings, settingsPath);
}
/**
* Returns the encryption key which is used to encrypt
* the credentials.
*
*/
export async function getEncryptionKey(): Promise<string> {
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
}
const userSettings = await getUserSettings();
if (userSettings?.encryptionKey === undefined) {
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
}
return userSettings.encryptionKey;
}
/**
* Returns the instance ID
*
*/
export async function getInstanceId(): Promise<string> {
const userSettings = await getUserSettings();
if (userSettings === undefined) {
return '';
}
if (userSettings.instanceId === undefined) {
return '';
}
return userSettings.instanceId;
}
async function generateInstanceId(key?: string) {
const hash = key
? createHash('sha256')
.update(key.slice(Math.round(key.length / 2)))
.digest('hex')
: undefined;
return hash;
}
/**
* Adds/Overwrite the given settings in the currently
* saved user settings
*
* @param {IUserSettings} addSettings The settings to add/overwrite
* @param {string} [settingsPath] Optional settings file path
*/
export async function addToUserSettings(
addSettings: IUserSettings,
settingsPath?: string,
): Promise<IUserSettings> {
if (settingsPath === undefined) {
settingsPath = getUserSettingsPath();
}
let userSettings = await getUserSettings(settingsPath);
if (userSettings === undefined) {
userSettings = {};
}
// Add the settings
Object.assign(userSettings, addSettings);
return writeUserSettings(userSettings, settingsPath);
}
/**
* Writes a user settings file
*
* @param {IUserSettings} userSettings The settings to write
* @param {string} [settingsPath] Optional settings file path
*/
export async function writeUserSettings(
userSettings: IUserSettings,
settingsPath?: string,
): Promise<IUserSettings> {
if (settingsPath === undefined) {
settingsPath = getUserSettingsPath();
}
if (userSettings === undefined) {
userSettings = {};
}
// Check if parent folder exists if not create it.
try {
await fsAccess(path.dirname(settingsPath));
} catch (error) {
// Parent folder does not exist so create
await fsMkdir(path.dirname(settingsPath));
}
const settingsToWrite = { ...userSettings };
if (settingsToWrite.instanceId !== undefined) {
delete settingsToWrite.instanceId;
}
await fsWriteFile(settingsPath, JSON.stringify(settingsToWrite, null, '\t'));
settingsCache = deepCopy(userSettings);
return userSettings;
}
/**
* Returns the content of the user settings
*
*/
export async function getUserSettings(
settingsPath?: string,
ignoreCache?: boolean,
): Promise<IUserSettings | undefined> {
if (settingsCache !== undefined && ignoreCache !== true) {
return settingsCache;
}
if (settingsPath === undefined) {
settingsPath = getUserSettingsPath();
}
try {
await fsAccess(settingsPath);
} catch (error) {
// The file does not exist
return undefined;
}
const settingsFile = await fsReadFile(settingsPath, 'utf8');
try {
settingsCache = JSON.parse(settingsFile);
} catch (error) {
throw new Error(
`Error parsing n8n-config file "${settingsPath}". It does not seem to be valid JSON.`,
);
}
return settingsCache as IUserSettings;
}
/**
* Returns the path to the user settings
*
*/
export function getUserSettingsPath(): string {
const n8nFolder = getUserN8nFolderPath();
return path.join(n8nFolder, USER_SETTINGS_FILE_NAME);
}
/**
* Returns the path to the n8n folder in which all n8n
* related data gets saved
*
*/
export function getUserN8nFolderPath(): string {
return path.join(getUserHome(), USER_SETTINGS_SUBFOLDER);
}
/**
* Returns the path to the n8n user folder with the custom
* extensions like nodes and credentials
*
*/
export function getUserN8nFolderCustomExtensionPath(): string {
return path.join(getUserN8nFolderPath(), EXTENSIONS_SUBDIRECTORY);
}
/**
* Returns the path to the n8n user folder with the nodes that
* have been downloaded
*
*/
export function getUserN8nFolderDownloadedNodesPath(): string {
return path.join(getUserN8nFolderPath(), DOWNLOADED_NODES_SUBDIRECTORY);
}
/**
* Returns the home folder path of the user if
* none can be found it falls back to the current
* working directory
*
*/
export function getUserHome(): string {
if (process.env[USER_FOLDER_ENV_OVERWRITE] !== undefined) {
return process.env[USER_FOLDER_ENV_OVERWRITE];
} else {
let variableName = 'HOME';
if (process.platform === 'win32') {
variableName = 'USERPROFILE';
}
if (process.env[variableName] === undefined) {
// If for some reason the variable does not exist
// fall back to current folder
return process.cwd();
}
return process.env[variableName] as string;
}
}

View file

@ -1,20 +1,21 @@
import * as NodeExecuteFunctions from './NodeExecuteFunctions'; import * as NodeExecuteFunctions from './NodeExecuteFunctions';
import * as UserSettings from './UserSettings';
export * from './ActiveWorkflows'; export * from './ActiveWorkflows';
export * from './BinaryData/BinaryData.service'; export * from './BinaryData/BinaryData.service';
export * from './BinaryData/types'; export * from './BinaryData/types';
export { Cipher } from './Cipher';
export * from './ClassLoader'; export * from './ClassLoader';
export * from './Constants'; export * from './Constants';
export * from './Credentials'; export * from './Credentials';
export * from './DirectoryLoader'; export * from './DirectoryLoader';
export * from './Interfaces'; export * from './Interfaces';
export { InstanceSettings } from './InstanceSettings';
export * from './LoadMappingOptions'; export * from './LoadMappingOptions';
export * from './LoadNodeParameterOptions'; export * from './LoadNodeParameterOptions';
export * from './LoadNodeListSearch'; export * from './LoadNodeListSearch';
export * from './NodeExecuteFunctions'; export * from './NodeExecuteFunctions';
export * from './WorkflowExecute'; export * from './WorkflowExecute';
export { NodeExecuteFunctions, UserSettings }; export { NodeExecuteFunctions };
export * from './errors'; export * from './errors';
export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee'; export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee';
export { BinaryData } from './BinaryData/types'; export { BinaryData } from './BinaryData/types';

View file

@ -1,23 +1,39 @@
import { Container } from 'typedi';
import { mock } from 'jest-mock-extended';
import type { CredentialInformation } from 'n8n-workflow';
import { Cipher } from '@/Cipher';
import { Credentials } from '@/Credentials'; import { Credentials } from '@/Credentials';
import type { InstanceSettings } from '@/InstanceSettings';
describe('Credentials', () => { describe('Credentials', () => {
const cipher = new Cipher(mock<InstanceSettings>({ encryptionKey: 'password' }));
Container.set(Cipher, cipher);
const setDataKey = (credentials: Credentials, key: string, data: CredentialInformation) => {
let fullData;
try {
fullData = credentials.getData();
} catch (e) {
fullData = {};
}
fullData[key] = data;
return credentials.setData(fullData);
};
describe('without nodeType set', () => { describe('without nodeType set', () => {
test('should be able to set and read key data without initial data set', () => { test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []); const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []);
const key = 'key1'; const key = 'key1';
const password = 'password';
// const nodeType = 'base.noOp';
const newData = 1234; const newData = 1234;
credentials.setDataKey(key, newData, password); setDataKey(credentials, key, newData);
expect(credentials.getDataKey(key, password)).toEqual(newData); expect(credentials.getData()[key]).toEqual(newData);
}); });
test('should be able to set and read key data with initial data set', () => { test('should be able to set and read key data with initial data set', () => {
const key = 'key2'; const key = 'key2';
const password = 'password';
// Saved under "key1" // Saved under "key1"
const initialData = 4321; const initialData = 4321;
@ -33,11 +49,11 @@ describe('Credentials', () => {
const newData = 1234; const newData = 1234;
// Set and read new data // Set and read new data
credentials.setDataKey(key, newData, password); setDataKey(credentials, key, newData);
expect(credentials.getDataKey(key, password)).toEqual(newData); expect(credentials.getData()[key]).toEqual(newData);
// Read the data which got provided encrypted on init // Read the data which got provided encrypted on init
expect(credentials.getDataKey('key1', password)).toEqual(initialData); expect(credentials.getData().key1).toEqual(initialData);
}); });
}); });
@ -54,19 +70,18 @@ describe('Credentials', () => {
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', nodeAccess); const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', nodeAccess);
const key = 'key1'; const key = 'key1';
const password = 'password';
const nodeType = 'base.noOp'; const nodeType = 'base.noOp';
const newData = 1234; const newData = 1234;
credentials.setDataKey(key, newData, password); setDataKey(credentials, key, newData);
// Should be able to read with nodeType which has access // Should be able to read with nodeType which has access
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData); expect(credentials.getData(nodeType)[key]).toEqual(newData);
// Should not be able to read with nodeType which does NOT have access // Should not be able to read with nodeType which does NOT have access
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error); // expect(credentials.getData('base.otherNode')[key]).toThrowError(Error);
try { try {
credentials.getDataKey(key, password, 'base.otherNode'); credentials.getData('base.otherNode');
expect(true).toBe(false); expect(true).toBe(false);
} catch (e) { } catch (e) {
expect(e.message).toBe( expect(e.message).toBe(

View file

@ -52,6 +52,7 @@ export class CredentialsHelper extends ICredentialsHelper {
} }
async getDecrypted( async getDecrypted(
additionalData: IWorkflowExecuteAdditionalData,
nodeCredentials: INodeCredentialsDetails, nodeCredentials: INodeCredentialsDetails,
type: string, type: string,
): Promise<ICredentialDataDecryptedObject> { ): Promise<ICredentialDataDecryptedObject> {
@ -128,15 +129,12 @@ export function WorkflowExecuteAdditionalData(
connections: {}, connections: {},
}; };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return { return {
credentialsHelper: new CredentialsHelper(''), credentialsHelper: new CredentialsHelper(),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo) => {}, executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo) => {},
sendMessageToUI: (message: string) => {}, sendDataToUI: (message: string) => {},
restApiUrl: '', restApiUrl: '',
encryptionKey: 'test',
timezone: 'America/New_York', timezone: 'America/New_York',
webhookBaseUrl: 'webhook', webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting', webhookWaitingBaseUrl: 'webhook-waiting',

View file

@ -1,4 +1,5 @@
import { UserSettings } from 'n8n-core'; import { Container } from 'typedi';
import { InstanceSettings } from 'n8n-core';
import { Command, flags } from '@oclif/command'; import { Command, flags } from '@oclif/command';
import type { IBuildOptions } from '../src'; import type { IBuildOptions } from '../src';
@ -17,7 +18,9 @@ export class Build extends Command {
help: flags.help({ char: 'h' }), help: flags.help({ char: 'h' }),
destination: flags.string({ destination: flags.string({
char: 'd', char: 'd',
description: `The path to copy the compiles files to [default: ${UserSettings.getUserN8nFolderCustomExtensionPath()}]`, description: `The path to copy the compiled files to [default: ${
Container.get(InstanceSettings).customExtensionDir
}]`,
}), }),
watch: flags.boolean({ watch: flags.boolean({
description: description:

View file

@ -59,6 +59,7 @@
"n8n-core": "workspace:*", "n8n-core": "workspace:*",
"n8n-workflow": "workspace:*", "n8n-workflow": "workspace:*",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"tmp-promise": "^3.0.3" "tmp-promise": "^3.0.3",
"typedi": "^0.10.0"
} }
} }

View file

@ -4,10 +4,11 @@ import glob from 'fast-glob';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises'; import { copyFile, mkdir, readFile, writeFile } from 'fs/promises';
import { join, dirname, resolve as resolvePath } from 'path'; import { join, dirname, resolve as resolvePath } from 'path';
import { Container } from 'typedi';
import { file as tmpFile } from 'tmp-promise'; import { file as tmpFile } from 'tmp-promise';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import { UserSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import type { IBuildOptions } from './Interfaces'; import type { IBuildOptions } from './Interfaces';
/** /**
@ -49,7 +50,7 @@ export async function createCustomTsconfig() {
* @param {IBuildOptions} [options] Options to overwrite default behavior * @param {IBuildOptions} [options] Options to overwrite default behavior
*/ */
export async function buildFiles({ export async function buildFiles({
destinationFolder = UserSettings.getUserN8nFolderCustomExtensionPath(), destinationFolder = Container.get(InstanceSettings).customExtensionDir,
watch, watch,
}: IBuildOptions): Promise<string> { }: IBuildOptions): Promise<string> {
const tscPath = join(dirname(require.resolve('typescript')), 'tsc'); const tscPath = join(dirname(require.resolve('typescript')), 'tsc');

View file

@ -2,4 +2,5 @@
module.exports = { module.exports = {
...require('../../jest.config'), ...require('../../jest.config'),
collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'], collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'],
setupFilesAfterEnv: ['jest-expect-message', '<rootDir>/test/setup.ts'],
}; };

View file

@ -226,7 +226,7 @@ function configureTransport(credentials: IDataObject, options: EmailSendOptions)
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;
const instanceId = await this.getInstanceId(); const instanceId = this.getInstanceId();
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
let item: INodeExecutionData; let item: INodeExecutionData;

View file

@ -229,7 +229,7 @@ export class FormTrigger implements INodeType {
if (webhookName === 'setup') { if (webhookName === 'setup') {
const formTitle = this.getNodeParameter('formTitle', '') as string; const formTitle = this.getNodeParameter('formTitle', '') as string;
const formDescription = this.getNodeParameter('formDescription', '') as string; const formDescription = this.getNodeParameter('formDescription', '') as string;
const instanceId = await this.getInstanceId(); const instanceId = this.getInstanceId();
const { formSubmittedText } = this.getNodeParameter('options', {}) as IDataObject; const { formSubmittedText } = this.getNodeParameter('options', {}) as IDataObject;
const data = prepareFormData( const data = prepareFormData(

View file

@ -206,7 +206,7 @@ export class GmailV2 implements INodeType {
const resource = this.getNodeParameter('resource', 0); const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0); const operation = this.getNodeParameter('operation', 0);
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;
const instanceId = await this.getInstanceId(); const instanceId = this.getInstanceId();
let responseData; let responseData;

View file

@ -271,7 +271,7 @@ export class MicrosoftTeams implements INodeType {
const resource = this.getNodeParameter('resource', 0); const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0); const operation = this.getNodeParameter('operation', 0);
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;
const instanceId = await this.getInstanceId(); const instanceId = this.getInstanceId();
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
try { try {

View file

@ -207,7 +207,6 @@ describe('Test MySql V2, operations', () => {
fakeConnectionCopy.query = jest.fn(async (query?: string) => { fakeConnectionCopy.query = jest.fn(async (query?: string) => {
const result = []; const result = [];
console.log(query);
if (query?.toLowerCase().includes('select')) { if (query?.toLowerCase().includes('select')) {
result.push([{ id: 1, name: 'test 1' }]); result.push([{ id: 1, name: 'test 1' }]);
} else { } else {

View file

@ -323,7 +323,7 @@ export class SlackV2 implements INodeType {
const operation = this.getNodeParameter('operation', 0); const operation = this.getNodeParameter('operation', 0);
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;
const instanceId = await this.getInstanceId(); const instanceId = this.getInstanceId();
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
try { try {

View file

@ -1708,7 +1708,7 @@ export class Telegram implements INodeType {
const binaryData = this.getNodeParameter('binaryData', 0, false); const binaryData = this.getNodeParameter('binaryData', 0, false);
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;
const instanceId = await this.getInstanceId(); const instanceId = this.getInstanceId();
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
try { try {

View file

@ -175,9 +175,8 @@ export function WorkflowExecuteAdditionalData(
credentialsHelper: new CredentialsHelper(credentialTypes), credentialsHelper: new CredentialsHelper(credentialTypes),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {}, executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
sendMessageToUI: (message: string) => {}, sendDataToUI: (message: string) => {},
restApiUrl: '', restApiUrl: '',
encryptionKey: 'test',
timezone: workflowTestData?.input.workflowData.settings?.timezone || 'America/New_York', timezone: workflowTestData?.input.workflowData.settings?.timezone || 'America/New_York',
webhookBaseUrl: 'webhook', webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting', webhookWaitingBaseUrl: 'webhook-waiting',

View file

@ -0,0 +1 @@
import 'reflect-metadata';

View file

@ -39,7 +39,7 @@
"dist/**/*" "dist/**/*"
], ],
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.3",
"@types/deep-equal": "^1.0.1", "@types/deep-equal": "^1.0.1",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/jmespath": "^0.15.0", "@types/jmespath": "^0.15.0",

View file

@ -108,17 +108,13 @@ export abstract class ICredentials {
this.data = data; this.data = data;
} }
abstract getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject; abstract getData(nodeType?: string): ICredentialDataDecryptedObject;
abstract getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation;
abstract getDataToSave(): ICredentialsEncrypted; abstract getDataToSave(): ICredentialsEncrypted;
abstract hasNodeAccess(nodeType: string): boolean; abstract hasNodeAccess(nodeType: string): boolean;
abstract setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void; abstract setData(data: ICredentialDataDecryptedObject): void;
abstract setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void;
} }
export interface IUser { export interface IUser {
@ -192,8 +188,6 @@ export interface IHttpRequestHelper {
helpers: { httpRequest: IAllExecuteFunctions['helpers']['httpRequest'] }; helpers: { httpRequest: IAllExecuteFunctions['helpers']['httpRequest'] };
} }
export abstract class ICredentialsHelper { export abstract class ICredentialsHelper {
constructor(readonly encryptionKey: string) {}
abstract getParentTypes(name: string): string[]; abstract getParentTypes(name: string): string[];
abstract authenticate( abstract authenticate(
@ -740,7 +734,7 @@ export interface FunctionsBase {
getTimezone(): string; getTimezone(): string;
getRestApiUrl(): string; getRestApiUrl(): string;
getInstanceBaseUrl(): string; getInstanceBaseUrl(): string;
getInstanceId(): Promise<string>; getInstanceId(): string;
getMode?: () => WorkflowExecuteMode; getMode?: () => WorkflowExecuteMode;
getActivationMode?: () => WorkflowActivateMode; getActivationMode?: () => WorkflowActivateMode;
@ -1847,7 +1841,6 @@ export interface IWorkflowExecuteHooks {
export interface IWorkflowExecuteAdditionalData { export interface IWorkflowExecuteAdditionalData {
credentialsHelper: ICredentialsHelper; credentialsHelper: ICredentialsHelper;
encryptionKey: string;
executeWorkflow: ( executeWorkflow: (
workflowInfo: IExecuteWorkflowInfo, workflowInfo: IExecuteWorkflowInfo,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
@ -1860,7 +1853,6 @@ export interface IWorkflowExecuteAdditionalData {
parentWorkflowSettings?: IWorkflowSettings; parentWorkflowSettings?: IWorkflowSettings;
}, },
) => Promise<any>; ) => Promise<any>;
// hooks?: IWorkflowExecuteHooks;
executionId?: string; executionId?: string;
restartExecutionId?: string; restartExecutionId?: string;
hooks?: WorkflowHooks; hooks?: WorkflowHooks;

View file

@ -1,6 +1,5 @@
import get from 'lodash/get'; import get from 'lodash/get';
import type { import type {
CredentialInformation,
IAdditionalCredentialOptions, IAdditionalCredentialOptions,
IAllExecuteFunctions, IAllExecuteFunctions,
IContextObject, IContextObject,
@ -44,48 +43,21 @@ export interface INodeTypesObject {
} }
export class Credentials extends ICredentials { export class Credentials extends ICredentials {
hasNodeAccess(nodeType: string): boolean { hasNodeAccess() {
return true; return true;
} }
setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void { setData(data: ICredentialDataDecryptedObject) {
this.data = JSON.stringify(data); this.data = JSON.stringify(data);
} }
setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void { getData(): ICredentialDataDecryptedObject {
let fullData;
try {
fullData = this.getData(encryptionKey);
} catch (e) {
fullData = {};
}
fullData[key] = data;
return this.setData(fullData, encryptionKey);
}
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
if (this.data === undefined) { if (this.data === undefined) {
throw new Error('No data is set so nothing can be returned.'); throw new Error('No data is set so nothing can be returned.');
} }
return JSON.parse(this.data); return JSON.parse(this.data);
} }
getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation {
const fullData = this.getData(encryptionKey, nodeType);
if (fullData === null) {
throw new Error('No data was set.');
}
if (!fullData.hasOwnProperty(key)) {
throw new Error(`No data for key "${key}" exists.`);
}
return fullData[key];
}
getDataToSave(): ICredentialsEncrypted { getDataToSave(): ICredentialsEncrypted {
if (this.data === undefined) { if (this.data === undefined) {
throw new Error('No credentials were set to save.'); throw new Error('No credentials were set to save.');
@ -702,12 +674,11 @@ export function WorkflowExecuteAdditionalData(): IWorkflowExecuteAdditionalData
}; };
return { return {
credentialsHelper: new CredentialsHelper(''), credentialsHelper: new CredentialsHelper(),
hooks: new WorkflowHooks({}, 'trigger', '1', workflowData), hooks: new WorkflowHooks({}, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {}, executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
sendMessageToUI: (message: string) => {}, sendDataToUI: (message: string) => {},
restApiUrl: '', restApiUrl: '',
encryptionKey: 'test',
timezone: 'America/New_York', timezone: 'America/New_York',
webhookBaseUrl: 'webhook', webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting', webhookWaitingBaseUrl: 'webhook-waiting',

View file

@ -260,9 +260,6 @@ importers:
cookie-parser: cookie-parser:
specifier: ^1.4.6 specifier: ^1.4.6
version: 1.4.6 version: 1.4.6
crypto-js:
specifier: ~4.1.1
version: 4.1.1
csrf: csrf:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0 version: 3.1.0
@ -586,7 +583,7 @@ importers:
specifier: ~1.7.2 specifier: ~1.7.2
version: 1.7.2 version: 1.7.2
crypto-js: crypto-js:
specifier: ~4.1.1 specifier: ^4.1.1
version: 4.1.1 version: 4.1.1
fast-glob: fast-glob:
specifier: ^3.2.5 specifier: ^3.2.5
@ -644,8 +641,8 @@ importers:
specifier: ~1.7.1 specifier: ~1.7.1
version: 1.7.3 version: 1.7.3
'@types/crypto-js': '@types/crypto-js':
specifier: ^4.0.1 specifier: ^4.1.3
version: 4.1.1 version: 4.1.3
'@types/express': '@types/express':
specifier: ^4.17.6 specifier: ^4.17.6
version: 4.17.14 version: 4.17.14
@ -1008,6 +1005,9 @@ importers:
tmp-promise: tmp-promise:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
typedi:
specifier: ^0.10.0
version: 0.10.0(patch_hash=62r6bc2crgimafeyruodhqlgo4)
devDependencies: devDependencies:
'@oclif/dev-cli': '@oclif/dev-cli':
specifier: ^1.22.2 specifier: ^1.22.2
@ -1348,8 +1348,8 @@ importers:
version: 0.5.0 version: 0.5.0
devDependencies: devDependencies:
'@types/crypto-js': '@types/crypto-js':
specifier: ^4.1.1 specifier: ^4.1.3
version: 4.1.1 version: 4.1.3
'@types/deep-equal': '@types/deep-equal':
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.0.1 version: 1.0.1
@ -6819,7 +6819,7 @@ packages:
ts-dedent: 2.2.0 ts-dedent: 2.2.0
type-fest: 3.13.1 type-fest: 3.13.1
vue: 3.3.4 vue: 3.3.4
vue-component-type-helpers: 1.8.15 vue-component-type-helpers: 1.8.19
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
@ -7054,8 +7054,8 @@ packages:
'@types/node': 18.16.16 '@types/node': 18.16.16
dev: true dev: true
/@types/crypto-js@4.1.1: /@types/crypto-js@4.1.3:
resolution: {integrity: sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==} resolution: {integrity: sha512-YP1sYYayLe7Eg5oXyLLvOLfxBfZ5Fgpz6sVWkpB18wDMywCLPWmqzRz+9gyuOoLF0fzDTTFwlyNbx7koONUwqA==}
dev: true dev: true
/@types/dateformat@3.0.1: /@types/dateformat@3.0.1:
@ -21794,8 +21794,8 @@ packages:
vue: 3.3.4 vue: 3.3.4
dev: false dev: false
/vue-component-type-helpers@1.8.15: /vue-component-type-helpers@1.8.19:
resolution: {integrity: sha512-RKiPRKW4BdwgmQ9vaNkHYKAThdTbgU4TOphVyyzqxRwsOJOoRIrb+vB49XLvs5CKPNrvxMXZMwPe5FyJCqFWyg==} resolution: {integrity: sha512-1OANGSZK4pzHF4uc86usWi+o5Y0zgoDtqWkPg6Am6ot+jHSAmpOah59V/4N82So5xRgivgCxGgK09lBy1XNUfQ==}
dev: true dev: true
/vue-component-type-helpers@1.8.4: /vue-component-type-helpers@1.8.4:
@ -22555,7 +22555,7 @@ packages:
dev: true dev: true
'@cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz': '@cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz':
resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz} resolution: {registry: https://registry.npmjs.org/, tarball: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz}
name: xlsx name: xlsx
version: 0.19.3 version: 0.19.3
engines: {node: '>=0.8'} engines: {node: '>=0.8'}