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',
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;
});

View file

@ -37,7 +37,7 @@
"@types/aws4": "^1.5.1",
"@types/concat-stream": "^2.0.0",
"@types/cron": "~1.7.1",
"@types/crypto-js": "^4.0.1",
"@types/crypto-js": "^4.1.3",
"@types/express": "^4.17.6",
"@types/lodash": "^4.14.195",
"@types/mime-types": "^2.1.0",
@ -54,7 +54,7 @@
"axios": "^0.21.1",
"concat-stream": "^2.0.0",
"cron": "~1.7.2",
"crypto-js": "~4.1.1",
"crypto-js": "^4.1.1",
"fast-glob": "^3.2.5",
"file-type": "^16.5.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 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_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';

View file

@ -1,13 +1,11 @@
import type {
CredentialInformation,
ICredentialDataDecryptedObject,
ICredentialsEncrypted,
} from 'n8n-workflow';
import { ICredentials } from 'n8n-workflow';
import { AES, enc } from 'crypto-js';
import { Container } from 'typedi';
import type { ICredentialDataDecryptedObject, ICredentialsEncrypted } from 'n8n-workflow';
import { ICredentials, jsonParse } from 'n8n-workflow';
import { Cipher } from './Cipher';
export class Credentials extends ICredentials {
private readonly cipher = Container.get(Cipher);
/**
* Returns if the given nodeType has access to data
*/
@ -24,30 +22,14 @@ export class Credentials extends ICredentials {
/**
* Sets new credential object
*/
setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void {
this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString();
}
/**
* 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);
setData(data: ICredentialDataDecryptedObject): void {
this.data = this.cipher.encrypt(data);
}
/**
* Returns the decrypted credential object
*/
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
getData(nodeType?: string): ICredentialDataDecryptedObject {
if (nodeType && !this.hasNodeAccess(nodeType)) {
throw new Error(
`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.');
}
const decryptedData = AES.decrypt(this.data, encryptionKey);
const decryptedData = this.cipher.decrypt(this.data);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(decryptedData.toString(enc.Utf8));
return jsonParse(decryptedData);
} catch (e) {
throw new Error(
'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
*/

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;
}
export interface IUserSettings {
encryptionKey?: string;
tunnelSubdomain?: string;
instanceId?: string;
}
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
errorWorkflow?: string;
timezone?: string;

View file

@ -3,14 +3,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-shadow */
import type {
ClientOAuth2Options,
ClientOAuth2RequestObject,
@ -143,9 +140,9 @@ import {
setWorkflowExecutionMetadata,
} from './WorkflowExecutionMetadata';
import { getSecretsProxy } from './Secrets';
import { getUserN8nFolderPath, getInstanceId } from './UserSettings';
import Container from 'typedi';
import type { BinaryData } from './BinaryData/types';
import { InstanceSettings } from './InstanceSettings';
axios.defaults.timeout = 300000;
// Prevent axios from adding x-form-www-urlencoded headers by default
@ -2510,7 +2507,7 @@ const getCommonWorkflowFunctions = (
getRestApiUrl: () => additionalData.restApiUrl,
getInstanceBaseUrl: () => additionalData.instanceBaseUrl,
getInstanceId: async () => getInstanceId(),
getInstanceId: () => Container.get(InstanceSettings).instanceId,
getTimezone: () => getTimezone(workflow, additionalData),
prepareOutputData: async (outputData) => [outputData],
@ -2600,7 +2597,6 @@ const getAllowedPaths = () => {
function isFilePathBlocked(filePath: string): boolean {
const allowedPaths = getAllowedPaths();
const resolvedFilePath = path.resolve(filePath);
const userFolder = getUserN8nFolderPath();
const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false';
//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
if (blockFileAccessToN8nFiles) {
const restrictedPaths: string[] = [userFolder];
const { n8nFolder } = Container.get(InstanceSettings);
const restrictedPaths = [n8nFolder];
if (process.env[CONFIG_FILES]) {
restrictedPaths.push(...process.env[CONFIG_FILES].split(','));
@ -2674,7 +2671,7 @@ const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunctions =>
},
getStoragePath() {
return path.join(getUserN8nFolderPath(), `storage/${node.type}`);
return path.join(Container.get(InstanceSettings).n8nFolder, `storage/${node.type}`);
},
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 UserSettings from './UserSettings';
export * from './ActiveWorkflows';
export * from './BinaryData/BinaryData.service';
export * from './BinaryData/types';
export { Cipher } from './Cipher';
export * from './ClassLoader';
export * from './Constants';
export * from './Credentials';
export * from './DirectoryLoader';
export * from './Interfaces';
export { InstanceSettings } from './InstanceSettings';
export * from './LoadMappingOptions';
export * from './LoadNodeParameterOptions';
export * from './LoadNodeListSearch';
export * from './NodeExecuteFunctions';
export * from './WorkflowExecute';
export { NodeExecuteFunctions, UserSettings };
export { NodeExecuteFunctions };
export * from './errors';
export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee';
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 type { InstanceSettings } from '@/InstanceSettings';
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', () => {
test('should be able to set and read key data without initial data set', () => {
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []);
const key = 'key1';
const password = 'password';
// const nodeType = 'base.noOp';
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', () => {
const key = 'key2';
const password = 'password';
// Saved under "key1"
const initialData = 4321;
@ -33,11 +49,11 @@ describe('Credentials', () => {
const newData = 1234;
// Set and read new data
credentials.setDataKey(key, newData, password);
expect(credentials.getDataKey(key, password)).toEqual(newData);
setDataKey(credentials, key, newData);
expect(credentials.getData()[key]).toEqual(newData);
// 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 key = 'key1';
const password = 'password';
const nodeType = 'base.noOp';
const newData = 1234;
credentials.setDataKey(key, newData, password);
setDataKey(credentials, key, newData);
// 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
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
// expect(credentials.getData('base.otherNode')[key]).toThrowError(Error);
try {
credentials.getDataKey(key, password, 'base.otherNode');
credentials.getData('base.otherNode');
expect(true).toBe(false);
} catch (e) {
expect(e.message).toBe(

View file

@ -52,6 +52,7 @@ export class CredentialsHelper extends ICredentialsHelper {
}
async getDecrypted(
additionalData: IWorkflowExecuteAdditionalData,
nodeCredentials: INodeCredentialsDetails,
type: string,
): Promise<ICredentialDataDecryptedObject> {
@ -128,15 +129,12 @@ export function WorkflowExecuteAdditionalData(
connections: {},
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return {
credentialsHelper: new CredentialsHelper(''),
credentialsHelper: new CredentialsHelper(),
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo) => {},
sendMessageToUI: (message: string) => {},
sendDataToUI: (message: string) => {},
restApiUrl: '',
encryptionKey: 'test',
timezone: 'America/New_York',
webhookBaseUrl: 'webhook',
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 type { IBuildOptions } from '../src';
@ -17,7 +18,9 @@ export class Build extends Command {
help: flags.help({ char: 'h' }),
destination: flags.string({
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({
description:

View file

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

View file

@ -2,4 +2,5 @@
module.exports = {
...require('../../jest.config'),
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[][]> {
const items = this.getInputData();
const nodeVersion = this.getNode().typeVersion;
const instanceId = await this.getInstanceId();
const instanceId = this.getInstanceId();
const returnData: INodeExecutionData[] = [];
let item: INodeExecutionData;

View file

@ -229,7 +229,7 @@ export class FormTrigger implements INodeType {
if (webhookName === 'setup') {
const formTitle = this.getNodeParameter('formTitle', '') 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 data = prepareFormData(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import get from 'lodash/get';
import type {
CredentialInformation,
IAdditionalCredentialOptions,
IAllExecuteFunctions,
IContextObject,
@ -44,48 +43,21 @@ export interface INodeTypesObject {
}
export class Credentials extends ICredentials {
hasNodeAccess(nodeType: string): boolean {
hasNodeAccess() {
return true;
}
setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void {
setData(data: ICredentialDataDecryptedObject) {
this.data = JSON.stringify(data);
}
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);
}
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
getData(): ICredentialDataDecryptedObject {
if (this.data === undefined) {
throw new Error('No data is set so nothing can be returned.');
}
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 {
if (this.data === undefined) {
throw new Error('No credentials were set to save.');
@ -702,12 +674,11 @@ export function WorkflowExecuteAdditionalData(): IWorkflowExecuteAdditionalData
};
return {
credentialsHelper: new CredentialsHelper(''),
credentialsHelper: new CredentialsHelper(),
hooks: new WorkflowHooks({}, 'trigger', '1', workflowData),
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
sendMessageToUI: (message: string) => {},
sendDataToUI: (message: string) => {},
restApiUrl: '',
encryptionKey: 'test',
timezone: 'America/New_York',
webhookBaseUrl: 'webhook',
webhookWaitingBaseUrl: 'webhook-waiting',

View file

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