diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index d946590a51..9c1d248a00 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -6,7 +6,11 @@ module.exports = { }, globalSetup: '/test/setup.ts', globalTeardown: '/test/teardown.ts', - setupFilesAfterEnv: ['/test/setup-mocks.ts', '/test/extend-expect.ts'], + setupFilesAfterEnv: [ + '/test/setup-test-folder.ts', + '/test/setup-mocks.ts', + '/test/extend-expect.ts', + ], coveragePathIgnorePatterns: ['/src/databases/migrations/'], testTimeout: 10_000, }; diff --git a/packages/cli/package.json b/packages/cli/package.json index ef1218b6c5..1246efe7ae 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/AbstractServer.ts index 1002476c5b..4376e02e3f 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/AbstractServer.ts @@ -45,8 +45,6 @@ export abstract class AbstractServer { protected endpointWebhookWaiting: string; - protected instanceId = ''; - protected webhooksEnabled = true; protected testWebhooksEnabled = false; diff --git a/packages/cli/src/CrashJournal.ts b/packages/cli/src/CrashJournal.ts index b0cd5ad3f8..7aa6c93731 100644 --- a/packages/cli/src/CrashJournal.ts +++ b/packages/cli/src/CrashJournal.ts @@ -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 => { } }; -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; diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index f8e08e7210..cc23fa6e5c 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -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 { 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 { 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 diff --git a/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts b/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts index d9cdab44be..18fd006fca 100644 --- a/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts +++ b/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts @@ -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 { @@ -86,15 +86,10 @@ export class ExternalSecretsManager { await Container.get(OrchestrationMainService).broadcastReloadExternalSecretsProviders(); } - private async getEncryptionKey(): Promise { - 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); } diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 6e3911de87..4f4c365b19 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -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, }), ]); } diff --git a/packages/cli/src/Ldap/helpers.ts b/packages/cli/src/Ldap/helpers.ts index 2ddbca37f0..aecf57399a 100644 --- a/packages/cli/src/Ldap/helpers.ts +++ b/packages/cli/src/Ldap/helpers.ts @@ -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 => { - 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 => { - 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 => { key: LDAP_FEATURE_NAME, }); const configurationData = jsonParse(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 => LdapManager.updateConfig({ ...ldapConfig }); - ldapConfig.bindingAdminPassword = await encryptPassword(ldapConfig.bindingAdminPassword); + ldapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(ldapConfig.bindingAdminPassword); if (!ldapConfig.loginEnabled) { ldapConfig.synchronizationEnabled = false; diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index 920bb1a739..668df04d20 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -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, }); diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index 9c1ddd900b..384e021d38 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -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> = []; + 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); } diff --git a/packages/cli/src/Mfa/mfa.service.ts b/packages/cli/src/Mfa/mfa.service.ts index 50b0d29f89..7e2ce1dd82 100644 --- a/packages/cli/src/Mfa/mfa.service.ts +++ b/packages/cli/src/Mfa/mfa.service.ts @@ -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) { diff --git a/packages/cli/src/Mfa/totp.service.ts b/packages/cli/src/Mfa/totp.service.ts index ee85c3f100..cbb1f65aac 100644 --- a/packages/cli/src/Mfa/totp.service.ts +++ b/packages/cli/src/Mfa/totp.service.ts @@ -1,4 +1,7 @@ import OTPAuth from 'otpauth'; +import { Service } from 'typedi'; + +@Service() export class TOTPService { generateSecret(): string { return new OTPAuth.Secret()?.base32; diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts index 22f814a209..4ce1488607 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts @@ -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'); diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts index d314151f95..b7c2412923 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.middleware.ts @@ -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'); diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts index b15640e160..25ab1e6dba 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts @@ -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 { - 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; } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index c6dd61317d..91cdaffbaf 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -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(); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 3a04a5a345..edbc93561c 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -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, diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index dee14511f8..3c8385f8e3 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -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 ?? {}; diff --git a/packages/cli/src/audit/risks/instance.risk.ts b/packages/cli/src/audit/risks/instance.risk.ts index 7465e6228d..0005047676 100644 --- a/packages/cli/src/audit/risks/instance.risk.ts +++ b/packages/cli/src/audit/risks/instance.risk.ts @@ -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(BASE_URL + currentVersionName, { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 8397034df5..0c9f3ae2a4 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -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 { 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'); diff --git a/packages/cli/src/commands/export/credentials.ts b/packages/cli/src/commands/export/credentials.ts index c47c75346f..6c1304e671 100644 --- a/packages/cli/src/commands/export/credentials.ts +++ b/packages/cli/src/commands/export/credentials.ts @@ -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; } } diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index 942d5c518b..81f724dda4 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -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); } diff --git a/packages/cli/src/commands/license/info.ts b/packages/cli/src/commands/license/info.ts index f4ebd406dc..dee106a581 100644 --- a/packages/cli/src/commands/license/info.ts +++ b/packages/cli/src/commands/license/info.ts @@ -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()); } diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index b6d01aa0e1..b2e4431c2e 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -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 = { diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 1f84a50ef6..15447cc222 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -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), diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 2c2890412f..5990955376 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -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(); diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 021b9f4588..829bf78304 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -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', }, diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index dcb4ab5e50..c9209f4c84 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -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', diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 24b4997476..c81f59fc61 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -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; } diff --git a/packages/cli/src/credentials/credentials.controller.ee.ts b/packages/cli/src/credentials/credentials.controller.ee.ts index 0aa25c4588..a5841a6831 100644 --- a/packages/cli/src/credentials/credentials.controller.ee.ts +++ b/packages/cli/src/credentials/credentials.controller.ee.ts @@ -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 => { 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); }), ); diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 098872cbc0..d7105b5bf5 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -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 => { 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, ); diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index c141952a68..f6f72a3ec4 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -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 { - 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 { + 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 { - const helper = new CredentialsHelper(encryptionKey); - + const helper = Container.get(CredentialsHelper); return helper.testCredentials(user, credentials.type, credentials); } diff --git a/packages/cli/src/credentials/oauth2Credential.api.ts b/packages/cli/src/credentials/oauth2Credential.api.ts index dda9b486e9..175b0f2bee 100644 --- a/packages/cli/src/credentials/oauth2Credential.api.ts +++ b/packages/cli/src/credentials/oauth2Credential.api.ts @@ -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(); diff --git a/packages/cli/src/databases/config.ts b/packages/cli/src/databases/config.ts index a8e42da625..1354fca0eb 100644 --- a/packages/cli/src/databases/config.ts +++ b/packages/cli/src/databases/config.ts @@ -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'), diff --git a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts index c3f164bbeb..48dfc3ad46 100644 --- a/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts +++ b/packages/cli/src/databases/migrations/sqlite/1690000000002-MigrateIntegerKeysToString.ts @@ -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); diff --git a/packages/cli/src/databases/utils/migrationHelpers.ts b/packages/cli/src/databases/utils/migrationHelpers.ts index dc50273806..0d1b45da1a 100644 --- a/packages/cli/src/databases/utils/migrationHelpers.ts +++ b/packages/cli/src/databases/utils/migrationHelpers.ts @@ -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; diff --git a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts index 365971cefc..5140421a72 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts @@ -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 { diff --git a/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts index 8d53fceae8..a87d21b34a 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlExport.service.ee.ts @@ -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 = { diff --git a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts index 85a6ad83fd..ebc45f464a 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts @@ -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> { - 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( - 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 { 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 || []; diff --git a/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts index ad99093196..52fe8b8f6c 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts @@ -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); } diff --git a/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts b/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts index 6af27c1b76..181fe82ca3 100644 --- a/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts +++ b/packages/cli/src/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee.ts @@ -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; diff --git a/packages/cli/src/eventbus/MessageEventBusWriter/MessageEventBusLogWriter.ts b/packages/cli/src/eventbus/MessageEventBusWriter/MessageEventBusLogWriter.ts index d004b061a8..f370e5b394 100644 --- a/packages/cli/src/eventbus/MessageEventBusWriter/MessageEventBusLogWriter.ts +++ b/packages/cli/src/eventbus/MessageEventBusWriter/MessageEventBusLogWriter.ts @@ -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: diff --git a/packages/cli/src/posthog/index.ts b/packages/cli/src/posthog/index.ts index df390c53b3..202a8cea97 100644 --- a/packages/cli/src/posthog/index.ts +++ b/packages/cli/src/posthog/index.ts @@ -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): Promise { 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 diff --git a/packages/cli/src/services/communityPackages.service.ts b/packages/cli/src/services/communityPackages.service.ts index a1fc549d1d..f8be2016e0 100644 --- a/packages/cli/src/services/communityPackages.service.ts +++ b/packages/cli/src/services/communityPackages.service.ts @@ -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, diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index fd776cc56c..c4f81b0aec 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -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'), diff --git a/packages/cli/src/services/orchestration/worker/types.ts b/packages/cli/src/services/orchestration/worker/types.ts index d95a3c5da3..351c56394a 100644 --- a/packages/cli/src/services/orchestration/worker/types.ts +++ b/packages/cli/src/services/orchestration/worker/types.ts @@ -3,7 +3,6 @@ import type { RedisServicePubSubPublisher } from '../../redis/RedisServicePubSub export interface WorkerCommandReceivedHandlerOptions { queueModeId: string; - instanceId: string; redisPublisher: RedisServicePubSubPublisher; getRunningJobIds: () => string[]; getRunningJobsSummary: () => WorkerJobStatusSummary[]; diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index 75b87a2c6b..349f8a6019 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -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 { + const { instanceId } = this.instanceSettings; return new Promise((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 { + const { instanceId } = this.instanceSettings; return new Promise((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, }; diff --git a/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts b/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts index 8ea55e7e73..2a6a4fc836 100644 --- a/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts +++ b/packages/cli/test/integration/ExternalSecrets/externalSecrets.api.test.ts @@ -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 { - 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(); diff --git a/packages/cli/test/integration/credentials.ee.test.ts b/packages/cli/test/integration/credentials.ee.test.ts index a1dc4f7be0..bc7222b850 100644 --- a/packages/cli/test/integration/credentials.ee.test.ts +++ b/packages/cli/test/integration/credentials.ee.test.ts @@ -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); diff --git a/packages/cli/test/integration/credentials.test.ts b/packages/cli/test/integration/credentials.test.ts index b62f0e9cab..3916865fab 100644 --- a/packages/cli/test/integration/credentials.test.ts +++ b/packages/cli/test/integration/credentials.test.ts @@ -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); diff --git a/packages/cli/test/integration/eventbus.ee.test.ts b/packages/cli/test/integration/eventbus.ee.test.ts index d8e5cf6137..c529109299 100644 --- a/packages/cli/test/integration/eventbus.ee.test.ts +++ b/packages/cli/test/integration/eventbus.ee.test.ts @@ -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); diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index c225bbffbb..521f9e6300 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -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'); }); diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index 9a17e401ce..5c9a01017f 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -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(); }); diff --git a/packages/cli/test/integration/publicApi/credentials.test.ts b/packages/cli/test/integration/publicApi/credentials.test.ts index bc5c414142..2a6e79ea06 100644 --- a/packages/cli/test/integration/publicApi/credentials.test.ts +++ b/packages/cli/test/integration/publicApi/credentials.test.ts @@ -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', () => { diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 910bfbb397..8fe1291bba 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -35,7 +35,6 @@ beforeAll(async () => { apiKey: randomApiKey(), }); - await utils.initEncryptionKey(); await utils.initNodeTypes(); workflowRunner = await utils.initActiveWorkflowRunner(); }); diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 7f8f2e33b8..287feb6d0f 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -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, 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; } diff --git a/packages/cli/test/integration/shared/utils/index.ts b/packages/cli/test/integration/shared/utils/index.ts index cd5d215f60..f4265e59e9 100644 --- a/packages/cli/test/integration/shared/utils/index.ts +++ b/packages/cli/test/integration/shared/utils/index.ts @@ -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. */ diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index 233cf8a76c..4bd2d90556 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -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) { diff --git a/packages/cli/test/integration/variables.test.ts b/packages/cli/test/integration/variables.test.ts index b0c0c458ef..701e6c4b08 100644 --- a/packages/cli/test/integration/variables.test.ts +++ b/packages/cli/test/integration/variables.test.ts @@ -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(); diff --git a/packages/cli/test/setup-test-folder.ts b/packages/cli/test/setup-test-folder.ts new file mode 100644 index 0000000000..07a8095373 --- /dev/null +++ b/packages/cli/test/setup-test-folder.ts @@ -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', +); diff --git a/packages/cli/test/unit/CredentialsHelper.test.ts b/packages/cli/test/unit/CredentialsHelper.test.ts index 96907b725b..52db57e27f 100644 --- a/packages/cli/test/unit/CredentialsHelper.test.ts +++ b/packages/cli/test/unit/CredentialsHelper.test.ts @@ -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, diff --git a/packages/cli/test/unit/ExternalSecrets/ExternalSecretsManager.test.ts b/packages/cli/test/unit/ExternalSecrets/ExternalSecretsManager.test.ts index 0789b87d67..60cdb04fcb 100644 --- a/packages/cli/test/unit/ExternalSecrets/ExternalSecretsManager.test.ts +++ b/packages/cli/test/unit/ExternalSecrets/ExternalSecretsManager.test.ts @@ -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({ - 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({ - 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'); diff --git a/packages/cli/test/unit/License.test.ts b/packages/cli/test/unit/License.test.ts index 99d90daac3..ab3319bb92 100644 --- a/packages/cli/test/unit/License.test.ts +++ b/packages/cli/test/unit/License.test.ts @@ -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, diff --git a/packages/cli/test/unit/PostHog.test.ts b/packages/cli/test/unit/PostHog.test.ts index 901f39c39f..6c53b44891 100644 --- a/packages/cli/test/unit/PostHog.test.ts +++ b/packages/cli/test/unit/PostHog.test.ts @@ -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, diff --git a/packages/cli/test/unit/SourceControl.test.ts b/packages/cli/test/unit/SourceControl.test.ts index d25678ea66..1123d177fe 100644 --- a/packages/cli/test/unit/SourceControl.test.ts +++ b/packages/cli/test/unit/SourceControl.test.ts @@ -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); diff --git a/packages/cli/test/unit/Telemetry.test.ts b/packages/cli/test/unit/Telemetry.test.ts index 4df728add1..8a38710fdf 100644 --- a/packages/cli/test/unit/Telemetry.test.ts +++ b/packages/cli/test/unit/Telemetry.test.ts @@ -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; }); diff --git a/packages/core/package.json b/packages/core/package.json index 8de88518f9..16960193aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/Cipher.ts b/packages/core/src/Cipher.ts new file mode 100644 index 0000000000..08af32a26c --- /dev/null +++ b/packages/core/src/Cipher.ts @@ -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); + } +} diff --git a/packages/core/src/Constants.ts b/packages/core/src/Constants.ts index f3fc5149e3..4ce7b66341 100644 --- a/packages/core/src/Constants.ts +++ b/packages/core/src/Constants.ts @@ -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'; diff --git a/packages/core/src/Credentials.ts b/packages/core/src/Credentials.ts index 7837fbcabb..00873c1da6 100644 --- a/packages/core/src/Credentials.ts +++ b/packages/core/src/Credentials.ts @@ -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 */ diff --git a/packages/core/src/InstanceSettings.ts b/packages/core/src/InstanceSettings.ts new file mode 100644 index 0000000000..d8ba2e616c --- /dev/null +++ b/packages/core/src/InstanceSettings.ts @@ -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'); + } +} diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index aa723045fd..777f9055a7 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -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; diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 03a4b4d8a1..8ba714ae5f 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -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) { diff --git a/packages/core/src/UserSettings.ts b/packages/core/src/UserSettings.ts deleted file mode 100644 index ee947c3061..0000000000 --- a/packages/core/src/UserSettings.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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; - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0325dcfdb8..6ec3e0cfde 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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'; diff --git a/packages/core/test/Credentials.test.ts b/packages/core/test/Credentials.test.ts index e2794b6c24..542d8eb8b6 100644 --- a/packages/core/test/Credentials.test.ts +++ b/packages/core/test/Credentials.test.ts @@ -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({ 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( diff --git a/packages/core/test/helpers/index.ts b/packages/core/test/helpers/index.ts index 8ffe9159e1..f5d46f34a0 100644 --- a/packages/core/test/helpers/index.ts +++ b/packages/core/test/helpers/index.ts @@ -52,6 +52,7 @@ export class CredentialsHelper extends ICredentialsHelper { } async getDecrypted( + additionalData: IWorkflowExecuteAdditionalData, nodeCredentials: INodeCredentialsDetails, type: string, ): Promise { @@ -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', diff --git a/packages/node-dev/commands/build.ts b/packages/node-dev/commands/build.ts index abca4e9205..b95abbab4d 100644 --- a/packages/node-dev/commands/build.ts +++ b/packages/node-dev/commands/build.ts @@ -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: diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 6a58f7200f..2df5cc00db 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -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" } } diff --git a/packages/node-dev/src/Build.ts b/packages/node-dev/src/Build.ts index b463bd1959..f600ca5297 100644 --- a/packages/node-dev/src/Build.ts +++ b/packages/node-dev/src/Build.ts @@ -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 { const tscPath = join(dirname(require.resolve('typescript')), 'tsc'); diff --git a/packages/nodes-base/jest.config.js b/packages/nodes-base/jest.config.js index 54696ffb30..61e5f7302d 100644 --- a/packages/nodes-base/jest.config.js +++ b/packages/nodes-base/jest.config.js @@ -2,4 +2,5 @@ module.exports = { ...require('../../jest.config'), collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'], + setupFilesAfterEnv: ['jest-expect-message', '/test/setup.ts'], }; diff --git a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts index c77730d947..0cd04aeb77 100644 --- a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts +++ b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts @@ -226,7 +226,7 @@ function configureTransport(credentials: IDataObject, options: EmailSendOptions) export async function execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const nodeVersion = this.getNode().typeVersion; - const instanceId = await this.getInstanceId(); + const instanceId = this.getInstanceId(); const returnData: INodeExecutionData[] = []; let item: INodeExecutionData; diff --git a/packages/nodes-base/nodes/Form/FormTrigger.node.ts b/packages/nodes-base/nodes/Form/FormTrigger.node.ts index 75f56584d4..24e32a0667 100644 --- a/packages/nodes-base/nodes/Form/FormTrigger.node.ts +++ b/packages/nodes-base/nodes/Form/FormTrigger.node.ts @@ -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( diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts index 6cb9f4f7c0..a16d9c65d9 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v2/GmailV2.node.ts @@ -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; diff --git a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts index eb07e7348d..04a1833a61 100644 --- a/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Teams/MicrosoftTeams.node.ts @@ -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 { diff --git a/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts b/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts index 9a7d4b544c..599ff8f9ef 100644 --- a/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts +++ b/packages/nodes-base/nodes/MySql/test/v2/operations.test.ts @@ -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 { diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index be1fe2079a..b707be5d31 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -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 { diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index 0e244a3b4f..3c3046bc82 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -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 { diff --git a/packages/nodes-base/test/nodes/Helpers.ts b/packages/nodes-base/test/nodes/Helpers.ts index 905a02ba81..1a13275a18 100644 --- a/packages/nodes-base/test/nodes/Helpers.ts +++ b/packages/nodes-base/test/nodes/Helpers.ts @@ -175,9 +175,8 @@ export function WorkflowExecuteAdditionalData( credentialsHelper: new CredentialsHelper(credentialTypes), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise => {}, - sendMessageToUI: (message: string) => {}, + sendDataToUI: (message: string) => {}, restApiUrl: '', - encryptionKey: 'test', timezone: workflowTestData?.input.workflowData.settings?.timezone || 'America/New_York', webhookBaseUrl: 'webhook', webhookWaitingBaseUrl: 'webhook-waiting', diff --git a/packages/nodes-base/test/setup.ts b/packages/nodes-base/test/setup.ts new file mode 100644 index 0000000000..d2c9bc6e64 --- /dev/null +++ b/packages/nodes-base/test/setup.ts @@ -0,0 +1 @@ +import 'reflect-metadata'; diff --git a/packages/workflow/package.json b/packages/workflow/package.json index cedab23d92..44bef7dfd8 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -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", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index dc61cd7114..22277e81ca 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -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; + 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; - // hooks?: IWorkflowExecuteHooks; executionId?: string; restartExecutionId?: string; hooks?: WorkflowHooks; diff --git a/packages/workflow/test/Helpers.ts b/packages/workflow/test/Helpers.ts index fededf3777..c01b8269e4 100644 --- a/packages/workflow/test/Helpers.ts +++ b/packages/workflow/test/Helpers.ts @@ -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 => {}, - sendMessageToUI: (message: string) => {}, + sendDataToUI: (message: string) => {}, restApiUrl: '', - encryptionKey: 'test', timezone: 'America/New_York', webhookBaseUrl: 'webhook', webhookWaitingBaseUrl: 'webhook-waiting', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f2a8330ff..11491f8a3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'}