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
94 changed files with 501 additions and 1070 deletions

View File

@@ -6,7 +6,11 @@ module.exports = {
},
globalSetup: '<rootDir>/test/setup.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/'],
testTimeout: 10_000,
};

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { existsSync } from 'fs';
import { mkdir, utimes, open, rm } from 'fs/promises';
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 { 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 () => {
if (!inProduction) return;

View File

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

View File

@@ -5,13 +5,12 @@ import type {
SecretsProviderSettings,
} from '@/Interfaces';
import { UserSettings } from 'n8n-core';
import { Cipher } from 'n8n-core';
import Container, { Service } from 'typedi';
import { AES, enc } from 'crypto-js';
import { getLogger } from '@/Logger';
import type { IDataObject } from 'n8n-workflow';
import { jsonParse, type IDataObject } from 'n8n-workflow';
import {
EXTERNAL_SECRETS_INITIAL_BACKOFF,
EXTERNAL_SECRETS_MAX_BACKOFF,
@@ -42,6 +41,7 @@ export class ExternalSecretsManager {
private settingsRepo: SettingsRepository,
private license: License,
private secretsProviders: ExternalSecretsProviders,
private cipher: Cipher,
) {}
async init(): Promise<void> {
@@ -86,15 +86,10 @@ export class ExternalSecretsManager {
await Container.get(OrchestrationMainService).broadcastReloadExternalSecretsProviders();
}
private async getEncryptionKey(): Promise<string> {
return UserSettings.getEncryptionKey();
}
private decryptSecretsSettings(value: string, encryptionKey: string): ExternalSecretsSettings {
const decryptedData = AES.decrypt(value, encryptionKey);
private decryptSecretsSettings(value: string): ExternalSecretsSettings {
const decryptedData = this.cipher.decrypt(value);
try {
return JSON.parse(decryptedData.toString(enc.Utf8)) as ExternalSecretsSettings;
return jsonParse(decryptedData);
} catch (e) {
throw new Error(
'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) {
return null;
}
const encryptionKey = await this.getEncryptionKey();
return this.decryptSecretsSettings(encryptedSettings, encryptionKey);
return this.decryptSecretsSettings(encryptedSettings);
}
private async internalInit() {
@@ -327,13 +321,12 @@ export class ExternalSecretsManager {
});
}
encryptSecretsSettings(settings: ExternalSecretsSettings, encryptionKey: string): string {
return AES.encrypt(JSON.stringify(settings), encryptionKey).toString();
private encryptSecretsSettings(settings: ExternalSecretsSettings): string {
return this.cipher.encrypt(settings);
}
async saveAndSetSettings(settings: ExternalSecretsSettings, settingsRepo: SettingsRepository) {
const encryptionKey = await this.getEncryptionKey();
const encryptedSettings = this.encryptSecretsSettings(settings, encryptionKey);
const encryptedSettings = this.encryptSecretsSettings(settings);
await settingsRepo.saveEncryptedSecretsProviderSettings(encryptedSettings);
}

View File

@@ -31,6 +31,7 @@ import { ExecutionRepository } from '@db/repositories';
import { RoleService } from './services/role.service';
import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
import { InstanceSettings } from 'n8n-core';
function userToPayload(user: User): {
userId: string;
@@ -50,22 +51,13 @@ function userToPayload(user: User): {
@Service()
export class InternalHooks implements IInternalHooksClass {
private instanceId: string;
public get telemetryInstanceId(): string {
return this.instanceId;
}
public get telemetryInstance(): Telemetry {
return this.telemetry;
}
constructor(
private telemetry: Telemetry,
private nodeTypes: NodeTypes,
private roleService: RoleService,
private executionRepository: ExecutionRepository,
eventsService: EventsService,
private readonly instanceSettings: InstanceSettings,
) {
eventsService.on('telemetry.onFirstProductionWorkflowSuccess', async (metrics) =>
this.onFirstProductionWorkflowSuccess(metrics),
@@ -75,9 +67,7 @@ export class InternalHooks implements IInternalHooksClass {
);
}
async init(instanceId: string) {
this.instanceId = instanceId;
this.telemetry.setInstanceId(instanceId);
async init() {
await this.telemetry.init();
}
@@ -813,7 +803,7 @@ export class InternalHooks implements IInternalHooksClass {
user_id: userCreatedCredentialsData.user.id,
credential_type: userCreatedCredentialsData.credential_type,
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_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added,
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 */
import { AES, enc } from 'crypto-js';
import type { Entry as LdapUser } from 'ldapts';
import { Filter } from 'ldapts/filters/Filter';
import { Container } from 'typedi';
import { UserSettings } from 'n8n-core';
import { Cipher } from 'n8n-core';
import { validate } from 'jsonschema';
import * as Db from '@/Db';
import config from '@/config';
@@ -110,22 +109,6 @@ export const validateLdapConfigurationSchema = (
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
*/
@@ -134,7 +117,7 @@ export const getLdapConfig = async (): Promise<LdapConfig> => {
key: LDAP_FEATURE_NAME,
});
const configurationData = jsonParse<LdapConfig>(configuration.value);
configurationData.bindingAdminPassword = await decryptPassword(
configurationData.bindingAdminPassword = Container.get(Cipher).decrypt(
configurationData.bindingAdminPassword,
);
return configurationData;
@@ -173,7 +156,7 @@ export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> =>
LdapManager.updateConfig({ ...ldapConfig });
ldapConfig.bindingAdminPassword = await encryptPassword(ldapConfig.bindingAdminPassword);
ldapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(ldapConfig.bindingAdminPassword);
if (!ldapConfig.loginEnabled) {
ldapConfig.synchronizationEnabled = false;

View File

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

View File

@@ -6,7 +6,7 @@ import fsPromises from 'fs/promises';
import type { DirectoryLoader, Types } from 'n8n-core';
import {
CUSTOM_EXTENSION_ENV,
UserSettings,
InstanceSettings,
CustomDirectoryLoader,
PackageDirectoryLoader,
LazyPackageDirectoryLoader,
@@ -47,10 +47,10 @@ export class LoadNodesAndCredentials {
includeNodes = config.getEnv('nodes.include');
private downloadFolder: string;
private postProcessors: Array<() => Promise<void>> = [];
constructor(private readonly instanceSettings: InstanceSettings) {}
async init() {
if (inTest) throw new Error('Not available in tests');
@@ -67,8 +67,6 @@ export class LoadNodesAndCredentials {
this.excludeNodes.push('n8n-nodes-base.e2eTest');
}
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
// Load nodes from `n8n-nodes-base`
const basePathsToScan = [
// 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
// 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.postProcessLoaders();
@@ -155,7 +155,7 @@ export class LoadNodesAndCredentials {
}
getCustomDirectories(): string[] {
const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()];
const customDirectories = [this.instanceSettings.customExtensionDir];
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV].split(';');
@@ -172,7 +172,11 @@ export class LoadNodesAndCredentials {
}
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);
}

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ export const validCredentialsProperties = (
): express.Response | void => {
const { type, data } = req.body;
const properties = new CredentialsHelper('')
const properties = Container.get(CredentialsHelper)
.getCredentialsProperties(type)
.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 * as Db from '@/Db';
import type { ICredentialsDb } from '@/Interfaces';
@@ -87,8 +87,6 @@ export async function removeCredential(credentials: CredentialsEntity): Promise<
}
export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> {
const encryptionKey = await UserSettings.getEncryptionKey();
// Encrypt the data
const coreCredential = new Credentials(
{ id: null, name: credential.name },
@@ -97,7 +95,7 @@ export async function encryptCredential(credential: CredentialsEntity): Promise<
);
// @ts-ignore
coreCredential.setData(credential.data, encryptionKey);
coreCredential.setData(credential.data);
return coreCredential.getDataToSave() as ICredentialsDb;
}

View File

@@ -30,7 +30,6 @@ import {
LoadMappingOptions,
LoadNodeParameterOptions,
LoadNodeListSearch,
UserSettings,
} from 'n8n-core';
import type {
@@ -146,7 +145,6 @@ import { SourceControlService } from '@/environments/sourceControl/sourceControl
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
import { ExecutionRepository, SettingsRepository } from '@db/repositories';
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { TOTPService } from './Mfa/totp.service';
import { MfaService } from './Mfa/mfa.service';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
import type { FrontendService } from './services/frontend.service';
@@ -159,25 +157,25 @@ import { WorkflowHistoryController } from './workflows/workflowHistory/workflowH
const exec = promisify(callbackExec);
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() {
super('main');
@@ -285,15 +283,13 @@ export class Server extends AbstractServer {
const repositories = Db.collections;
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
const encryptionKey = await UserSettings.getEncryptionKey();
const logger = LoggerProxy;
const internalHooks = Container.get(InternalHooks);
const mailer = Container.get(UserManagementMailer);
const userService = Container.get(UserService);
const jwtService = Container.get(JwtService);
const postHog = this.postHog;
const mfaService = new MfaService(repositories.User, new TOTPService(), encryptionKey);
const mfaService = Container.get(MfaService);
const controllers: object[] = [
new EventBusController(),
@@ -376,19 +372,16 @@ export class Server extends AbstractServer {
await Container.get(MetricsService).configureMetrics(this.app);
}
this.instanceId = await UserSettings.getInstanceId();
this.frontendService.addToSettings({
isNpmAvailable: await exec('npm --version')
.then(() => true)
.catch(() => false),
versionCli: N8N_VERSION,
instanceId: this.instanceId,
});
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 excludeEndpoints = config.getEnv('security.excludeEndpoints');
@@ -739,18 +732,11 @@ export class Server extends AbstractServer {
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 mode: WorkflowExecuteMode = 'internal';
const timezone = config.getEnv('generic.timezone');
const credentialsHelper = new CredentialsHelper(encryptionKey);
const credentialsHelper = Container.get(CredentialsHelper);
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
additionalData,
credential as INodeCredentialsDetails,
@@ -835,7 +821,7 @@ export class Server extends AbstractServer {
credential.nodesAccess,
);
credentials.setData(decryptedDataOriginal, encryptionKey);
credentials.setData(decryptedDataOriginal);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data
@@ -889,18 +875,11 @@ export class Server extends AbstractServer {
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 mode: WorkflowExecuteMode = 'internal';
const timezone = config.getEnv('generic.timezone');
const credentialsHelper = new CredentialsHelper(encryptionKey);
const credentialsHelper = Container.get(CredentialsHelper);
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
additionalData,
credential as INodeCredentialsDetails,
@@ -952,7 +931,7 @@ export class Server extends AbstractServer {
credential.type,
credential.nodesAccess,
);
credentials.setData(decryptedDataOriginal, encryptionKey);
credentials.setData(decryptedDataOriginal);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data
newCredentialsData.updatedAt = new Date();

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import axios from 'axios';
import { UserSettings } from 'n8n-core';
import { Container } from 'typedi';
import { InstanceSettings } from 'n8n-core';
import config from '@/config';
import { toFlaggedNode } from '@/audit/utils';
import { separate } from '@/utils';
@@ -81,7 +82,7 @@ function getUnprotectedWebhookNodes(workflows: WorkflowEntity[]) {
async function getNextVersions(currentVersionName: string) {
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, {
// 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 { Container } from 'typedi';
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import type { IUserSettings } from 'n8n-core';
import { BinaryDataService, ObjectStoreService, UserSettings } from 'n8n-core';
import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
import type { AbstractServer } from '@/AbstractServer';
import { getLogger } from '@/Logger';
import config from '@/config';
@@ -30,11 +29,9 @@ export abstract class BaseCommand extends Command {
protected nodeTypes: NodeTypes;
protected userSettings: IUserSettings;
protected instanceSettings: InstanceSettings;
protected instanceId: string;
instanceType: N8nInstanceType = 'main';
private instanceType: N8nInstanceType = 'main';
queueModeId: string;
@@ -48,7 +45,7 @@ export abstract class BaseCommand extends Command {
process.once('SIGINT', async () => this.stopProcess());
// Make sure the settings exist
this.userSettings = await UserSettings.prepareUserSettings();
this.instanceSettings = Container.get(InstanceSettings);
await Container.get(LoadNodesAndCredentials).init();
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(this.instanceId);
await Container.get(InternalHooks).init(this.instanceId);
await Container.get(PostHogClient).init();
await Container.get(InternalHooks).init();
}
protected setInstanceType(instanceType: N8nInstanceType) {
@@ -241,7 +237,7 @@ export abstract class BaseCommand extends Command {
async initLicense(): Promise<void> {
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');

View File

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

View File

@@ -72,8 +72,6 @@ export class ImportCredentialsCommand extends BaseCommand {
await this.initOwnerCredentialRole();
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
const encryptionKey = this.userSettings.encryptionKey;
if (flags.separate) {
let { input: inputPath } = flags;
@@ -97,7 +95,7 @@ export class ImportCredentialsCommand extends BaseCommand {
if (typeof credential.data === 'object') {
// 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);
@@ -125,7 +123,7 @@ export class ImportCredentialsCommand extends BaseCommand {
for (const credential of credentials) {
if (typeof credential.data === 'object') {
// 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);
}

View File

@@ -9,7 +9,7 @@ export class LicenseInfoCommand extends BaseCommand {
async run() {
const license = Container.get(License);
await license.init(this.instanceId);
await license.init();
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 { createReadStream, createWriteStream, existsSync } from 'fs';
import localtunnel from 'localtunnel';
import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core';
import { flags } from '@oclif/command';
import stream from 'stream';
import replaceStream from 'replacestream';
@@ -245,7 +244,7 @@ export class Start extends BaseCommand {
if (!config.getEnv('userManagement.jwtSecret')) {
// If we don't have a JWT secret set, generate
// 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
// 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'));
}
await UserSettings.getEncryptionKey();
// Load settings from database and set them to config.
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
databaseSettings.forEach((setting) => {
@@ -285,28 +282,19 @@ export class Start extends BaseCommand {
if (flags.tunnel) {
this.log('\nWaiting for tunnel ...');
let tunnelSubdomain;
if (
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;
}
let tunnelSubdomain =
process.env.N8N_TUNNEL_SUBDOMAIN ?? this.instanceSettings.tunnelSubdomain ?? '';
if (tunnelSubdomain === undefined) {
if (tunnelSubdomain === '') {
// When no tunnel subdomain did exist yet create a new random one
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
this.userSettings.tunnelSubdomain = Array.from({ length: 24 })
.map(() => {
return availableCharacters.charAt(
Math.floor(Math.random() * availableCharacters.length),
);
})
tunnelSubdomain = Array.from({ length: 24 })
.map(() =>
availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length)),
)
.join('');
await UserSettings.writeUserSettings(this.userSettings);
this.instanceSettings.update({ tunnelSubdomain });
}
const tunnelSettings: localtunnel.TunnelConfig = {

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { readFileSync } from 'fs';
import { resolve, join, dirname } from 'path';
import { Container } from 'typedi';
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';
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 TEMPLATES_DIR = join(CLI_DIR, 'templates');
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 function getN8nPackageJson() {
@@ -34,7 +38,6 @@ export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
export const RESPONSE_ERROR_MESSAGES = {
NO_CREDENTIAL: 'Credential 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_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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { statSync } from 'fs';
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 config from '@/config';
@@ -191,7 +192,7 @@ const migrationsPruningEnabled = process.env.MIGRATIONS_PRUNING_ENABLED === 'tru
function getSqliteDbFileSize(): number {
const filename = path.resolve(
UserSettings.getUserN8nFolderPath(),
Container.get(InstanceSettings).n8nFolder,
config.getEnv('database.sqlite.database'),
);
const { size } = statSync(filename);

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import * as Db from '@/Db';
import glob from 'fast-glob';
import { LoggerProxy, jsonParse } from 'n8n-workflow';
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 { ExportableCredential } from './types/exportableCredential';
import type { Variables } from '@db/entities/Variables';
@@ -41,9 +41,9 @@ export class SourceControlImportService {
private readonly variablesService: VariablesService,
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
private readonly tagRepository: TagRepository,
instanceSettings: InstanceSettings,
) {
const userFolder = UserSettings.getUserN8nFolderPath();
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
this.credentialExportFolder = path.join(
this.gitFolder,
@@ -81,69 +81,6 @@ export class SourceControlImportService {
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[]> {
const remoteWorkflowFiles = await glob('*.json', {
cwd: this.workflowExportFolder,
@@ -407,7 +344,6 @@ export class SourceControlImportService {
roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]),
},
});
const encryptionKey = await UserSettings.getEncryptionKey();
let importCredentialsResult: Array<{ id: string; name: string; type: string }> = [];
importCredentialsResult = await Promise.all(
candidates.map(async (candidate) => {
@@ -427,7 +363,7 @@ export class SourceControlImportService {
if (existingCredential?.data) {
newCredentialObject.data = existingCredential.data;
} else {
newCredentialObject.setData(data, encryptionKey);
newCredentialObject.setData(data);
}
newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || [];

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { Service } from 'typedi';
import type { PostHog } from 'posthog-node';
import type { FeatureFlags, ITelemetryTrackProperties } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core';
import config from '@/config';
import type { PublicUser } from '@/Interfaces';
@@ -8,10 +9,9 @@ import type { PublicUser } from '@/Interfaces';
export class PostHogClient {
private postHog?: PostHog;
private instanceId?: string;
constructor(private readonly instanceSettings: InstanceSettings) {}
async init(instanceId: string) {
this.instanceId = instanceId;
async init() {
const enabled = config.getEnv('diagnostics.enabled');
if (!enabled) {
return;
@@ -46,7 +46,7 @@ export class PostHogClient {
async getFeatureFlags(user: Pick<PublicUser, 'id' | 'createdAt'>): Promise<FeatureFlags> {
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
// 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 type { PublicInstalledPackage } from 'n8n-workflow';
import { UserSettings } from 'n8n-core';
import { InstanceSettings } from 'n8n-core';
import type { PackageDirectoryLoader } from 'n8n-core';
import { toError } from '@/utils';
@@ -47,6 +47,7 @@ export class CommunityPackagesService {
missingPackages: string[] = [];
constructor(
private readonly instanceSettings: InstanceSettings,
private readonly installedPackageRepository: InstalledPackagesRepository,
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
) {}
@@ -114,7 +115,7 @@ export class CommunityPackagesService {
}
async executeNpmCommand(command: string, options?: { doNotHandleError?: boolean }) {
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const downloadFolder = this.instanceSettings.nodesDownloadDir;
const execOptions = {
cwd: downloadFolder,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,8 @@
import type { SuperAgentTest } from 'supertest';
import { In } from 'typeorm';
import { UserSettings } from 'n8n-core';
import type { IUser } from 'n8n-workflow';
import * as Db from '@/Db';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { Credentials } from '@/requests';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
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
});
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 () => {
const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404);

View File

@@ -1,9 +1,7 @@
import type { SuperAgentTest } from 'supertest';
import { UserSettings } from 'n8n-core';
import * as Db from '@/Db';
import config from '@/config';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import type { Credentials } from '@/requests';
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 () => {
const firstResponse = await authOwnerAgent
.post('/credentials')
@@ -385,17 +372,6 @@ describe('PATCH /credentials/:id', () => {
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', () => {
@@ -504,21 +480,6 @@ describe('GET /credentials/:id', () => {
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 () => {
const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404);

View File

@@ -89,7 +89,6 @@ beforeAll(async () => {
mockedSyslog.createClient.mockImplementation(() => new syslog.Client());
await utils.initEncryptionKey();
config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter');
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 { LdapManager } from '@/Ldap/LdapManager.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 { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { randomEmail, randomName, uniqueId } from './../shared/random';
import * as testDb from './../shared/testDb';
import * as utils from '../shared/utils/';
import Container from 'typedi';
import { Cipher } from 'n8n-core';
jest.mock('@/telemetry');
@@ -54,12 +56,10 @@ beforeAll(async () => {
owner = await testDb.createUser({ globalRole: globalOwnerRole });
authOwnerAgent = testServer.authAgentFor(owner);
defaultLdapConfig.bindingAdminPassword = await encryptPassword(
defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(
defaultLdapConfig.bindingAdminPassword,
);
await utils.initEncryptionKey();
await setCurrentAuthenticationMethod('email');
});

View File

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

View File

@@ -1,9 +1,7 @@
import type { SuperAgentTest } from 'supertest';
import { UserSettings } from 'n8n-core';
import * as Db from '@/Db';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { randomApiKey, randomName, randomString } from '../shared/random';
import * as utils from '../shared/utils/';
@@ -22,9 +20,6 @@ let saveCredential: SaveCredentialFunction;
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
beforeAll(async () => {
// TODO: mock encryption key
await utils.initEncryptionKey();
const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] =
await testDb.getAllRoles();
@@ -87,17 +82,6 @@ describe('POST /credentials', () => {
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', () => {

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import { Container } from 'typedi';
import { randomBytes } from 'crypto';
import { existsSync } from 'fs';
import { BinaryDataService, UserSettings } from 'n8n-core';
import { BinaryDataService } from 'n8n-core';
import type { INode } from 'n8n-workflow';
import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.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);
}
/**
* 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.
*/

View File

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

View File

@@ -16,7 +16,6 @@ const licenseLike = {
const testServer = utils.setupTestServer({ endpointGroups: ['variables'] });
beforeAll(async () => {
await utils.initEncryptionKey();
utils.mockInstance(License, licenseLike);
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 { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils';
import Container from 'typedi';
describe('CredentialsHelper', () => {
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(
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 { License } from '@/License';
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee';
import { mock } from 'jest-mock-extended';
import { UserSettings } from 'n8n-core';
import Container from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import { mockInstance } from '../../integration/shared/utils';
import {
DummyProvider,
@@ -13,56 +13,42 @@ import {
FailedProvider,
MockProviders,
} 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', () => {
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(() => {
jest
.spyOn(UserSettings, 'getEncryptionKey')
.mockReturnValue(new Promise((resolve) => resolve(encryptionKey)));
providersMock = mockInstance(ExternalSecretsProviders, mockProvidersInstance);
licenseMock = mockInstance(License, {
isExternalSecretsEnabled() {
return true;
},
settings = createMockSettings({
dummy: { connected: true, connectedAt: new Date(connectedDate), settings: {} },
});
mockInstance(InternalHooks);
});
beforeEach(() => {
mockProvidersInstance.setProviders({
dummy: DummyProvider,
});
settings = createMockSettings({
dummy: { connected: true, connectedAt: new Date(connectedDate), settings: {} },
});
Container.remove(ExternalSecretsManager);
license.isExternalSecretsEnabled.mockReturnValue(true);
settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
manager = new ExternalSecretsManager(settingsRepo, license, providersMock, cipher);
});
afterEach(() => {
@@ -71,8 +57,6 @@ describe('External Secrets Manager', () => {
});
test('should get secret', async () => {
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init();
expect(manager.getSecret('dummy', 'test1')).toBe('value1');
@@ -82,8 +66,6 @@ describe('External Secrets Manager', () => {
mockProvidersInstance.setProviders({
dummy: ErrorProvider,
});
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
expect(async () => manager!.init()).not.toThrow();
});
@@ -91,16 +73,12 @@ describe('External Secrets Manager', () => {
mockProvidersInstance.setProviders({
dummy: ErrorProvider,
});
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init();
expect(() => manager!.shutdown()).not.toThrow();
manager = undefined;
});
test('should save provider settings', async () => {
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
const settingsSpy = jest.spyOn(settingsRepo, 'saveEncryptedSecretsProviderSettings');
await manager.init();
@@ -122,8 +100,6 @@ describe('External Secrets Manager', () => {
test('should call provider update functions on a timer', async () => {
jest.useFakeTimers();
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init();
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 () => {
jest.useFakeTimers();
manager = new ExternalSecretsManager(
settingsRepo,
mock<License>({
isExternalSecretsEnabled() {
return false;
},
}),
providersMock,
);
license.isExternalSecretsEnabled.mockReturnValue(false);
await manager.init();
@@ -165,7 +133,6 @@ describe('External Secrets Manager', () => {
mockProvidersInstance.setProviders({
dummy: FailedProvider,
});
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init();
@@ -179,8 +146,6 @@ describe('External Secrets Manager', () => {
});
test('should reinitialize a provider when save provider settings', async () => {
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
await manager.init();
const dummyInitSpy = jest.spyOn(DummyProvider.prototype, 'init');

View File

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

View File

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

View File

@@ -9,12 +9,11 @@ import {
} from '@/environments/sourceControl/sourceControlHelper.ee';
import { License } from '@/License';
import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee';
import { UserSettings } from 'n8n-core';
import { InstanceSettings } from 'n8n-core';
import path from 'path';
import {
SOURCE_CONTROL_SSH_FOLDER,
SOURCE_CONTROL_GIT_FOLDER,
SOURCE_CONTROL_SSH_KEY_NAME,
} from '@/environments/sourceControl/constants';
import { LoggerProxy } from 'n8n-workflow';
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 () => {
const userFolder = UserSettings.getUserN8nFolderPath();
const sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER);
const gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
const sshKeyName = path.join(sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
const { n8nFolder } = Container.get(InstanceSettings);
const sshFolder = path.join(n8nFolder, SOURCE_CONTROL_SSH_FOLDER);
const gitFolder = path.join(n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
let hasThrown = false;
try {
accessSync(sshFolder, fsConstants.F_OK);

View File

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