mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-23 10:32:17 -08:00
refactor(core): Abstract away InstanceSettings and encryptionKey
into injectable services (no-changelog) (#7471)
This change ensures that things like `encryptionKey` and `instanceId` are always available directly where they are needed, instead of passing them around throughout the code.
This commit is contained in:
parent
519680c2cf
commit
b6de910cbe
|
@ -6,7 +6,11 @@ module.exports = {
|
||||||
},
|
},
|
||||||
globalSetup: '<rootDir>/test/setup.ts',
|
globalSetup: '<rootDir>/test/setup.ts',
|
||||||
globalTeardown: '<rootDir>/test/teardown.ts',
|
globalTeardown: '<rootDir>/test/teardown.ts',
|
||||||
setupFilesAfterEnv: ['<rootDir>/test/setup-mocks.ts', '<rootDir>/test/extend-expect.ts'],
|
setupFilesAfterEnv: [
|
||||||
|
'<rootDir>/test/setup-test-folder.ts',
|
||||||
|
'<rootDir>/test/setup-mocks.ts',
|
||||||
|
'<rootDir>/test/extend-expect.ts',
|
||||||
|
],
|
||||||
coveragePathIgnorePatterns: ['/src/databases/migrations/'],
|
coveragePathIgnorePatterns: ['/src/databases/migrations/'],
|
||||||
testTimeout: 10_000,
|
testTimeout: 10_000,
|
||||||
};
|
};
|
||||||
|
|
|
@ -121,7 +121,6 @@
|
||||||
"connect-history-api-fallback": "^1.6.0",
|
"connect-history-api-fallback": "^1.6.0",
|
||||||
"convict": "^6.2.4",
|
"convict": "^6.2.4",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"crypto-js": "~4.1.1",
|
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"curlconverter": "3.21.0",
|
"curlconverter": "3.21.0",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
|
|
|
@ -45,8 +45,6 @@ export abstract class AbstractServer {
|
||||||
|
|
||||||
protected endpointWebhookWaiting: string;
|
protected endpointWebhookWaiting: string;
|
||||||
|
|
||||||
protected instanceId = '';
|
|
||||||
|
|
||||||
protected webhooksEnabled = true;
|
protected webhooksEnabled = true;
|
||||||
|
|
||||||
protected testWebhooksEnabled = false;
|
protected testWebhooksEnabled = false;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { mkdir, utimes, open, rm } from 'fs/promises';
|
import { mkdir, utimes, open, rm } from 'fs/promises';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Container } from 'typedi';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import { LoggerProxy, sleep } from 'n8n-workflow';
|
import { LoggerProxy, sleep } from 'n8n-workflow';
|
||||||
import { inProduction } from '@/constants';
|
import { inProduction } from '@/constants';
|
||||||
|
|
||||||
|
@ -16,7 +17,8 @@ export const touchFile = async (filePath: string): Promise<void> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const journalFile = join(UserSettings.getUserN8nFolderPath(), 'crash.journal');
|
const { n8nFolder } = Container.get(InstanceSettings);
|
||||||
|
const journalFile = join(n8nFolder, 'crash.journal');
|
||||||
|
|
||||||
export const init = async () => {
|
export const init = async () => {
|
||||||
if (!inProduction) return;
|
if (!inProduction) return;
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
|
||||||
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
|
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
|
@ -53,7 +52,7 @@ import { CredentialTypes } from '@/CredentialTypes';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { whereClause } from './UserManagement/UserManagementHelper';
|
import { whereClause } from './UserManagement/UserManagementHelper';
|
||||||
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
||||||
import { Container } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { isObjectLiteral } from './utils';
|
import { isObjectLiteral } from './utils';
|
||||||
|
|
||||||
const { OAUTH2_CREDENTIAL_TEST_SUCCEEDED, OAUTH2_CREDENTIAL_TEST_FAILED } = RESPONSE_ERROR_MESSAGES;
|
const { OAUTH2_CREDENTIAL_TEST_SUCCEEDED, OAUTH2_CREDENTIAL_TEST_FAILED } = RESPONSE_ERROR_MESSAGES;
|
||||||
|
@ -87,12 +86,15 @@ const mockNodeTypes: INodeTypes = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class CredentialsHelper extends ICredentialsHelper {
|
export class CredentialsHelper extends ICredentialsHelper {
|
||||||
private credentialTypes = Container.get(CredentialTypes);
|
constructor(
|
||||||
|
private readonly credentialTypes: CredentialTypes,
|
||||||
private nodeTypes = Container.get(NodeTypes);
|
private readonly nodeTypes: NodeTypes,
|
||||||
|
private readonly credentialsOverwrites: CredentialsOverwrites,
|
||||||
private credentialsOverwrites = Container.get(CredentialsOverwrites);
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the required authentication information to the request
|
* Add the required authentication information to the request
|
||||||
|
@ -349,7 +351,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
expressionResolveValues?: ICredentialsExpressionResolveValues,
|
||||||
): Promise<ICredentialDataDecryptedObject> {
|
): Promise<ICredentialDataDecryptedObject> {
|
||||||
const credentials = await this.getCredentials(nodeCredentials, type);
|
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||||
const decryptedDataOriginal = credentials.getData(this.encryptionKey);
|
const decryptedDataOriginal = credentials.getData();
|
||||||
|
|
||||||
if (raw === true) {
|
if (raw === true) {
|
||||||
return decryptedDataOriginal;
|
return decryptedDataOriginal;
|
||||||
|
@ -469,7 +471,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const credentials = await this.getCredentials(nodeCredentials, type);
|
const credentials = await this.getCredentials(nodeCredentials, type);
|
||||||
|
|
||||||
credentials.setData(data, this.encryptionKey);
|
credentials.setData(data);
|
||||||
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as ICredentialsDb;
|
||||||
|
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
|
|
|
@ -5,13 +5,12 @@ import type {
|
||||||
SecretsProviderSettings,
|
SecretsProviderSettings,
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
|
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Cipher } from 'n8n-core';
|
||||||
import Container, { Service } from 'typedi';
|
import Container, { Service } from 'typedi';
|
||||||
|
|
||||||
import { AES, enc } from 'crypto-js';
|
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
|
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import { jsonParse, type IDataObject } from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
EXTERNAL_SECRETS_INITIAL_BACKOFF,
|
EXTERNAL_SECRETS_INITIAL_BACKOFF,
|
||||||
EXTERNAL_SECRETS_MAX_BACKOFF,
|
EXTERNAL_SECRETS_MAX_BACKOFF,
|
||||||
|
@ -42,6 +41,7 @@ export class ExternalSecretsManager {
|
||||||
private settingsRepo: SettingsRepository,
|
private settingsRepo: SettingsRepository,
|
||||||
private license: License,
|
private license: License,
|
||||||
private secretsProviders: ExternalSecretsProviders,
|
private secretsProviders: ExternalSecretsProviders,
|
||||||
|
private cipher: Cipher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
@ -86,15 +86,10 @@ export class ExternalSecretsManager {
|
||||||
await Container.get(OrchestrationMainService).broadcastReloadExternalSecretsProviders();
|
await Container.get(OrchestrationMainService).broadcastReloadExternalSecretsProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getEncryptionKey(): Promise<string> {
|
private decryptSecretsSettings(value: string): ExternalSecretsSettings {
|
||||||
return UserSettings.getEncryptionKey();
|
const decryptedData = this.cipher.decrypt(value);
|
||||||
}
|
|
||||||
|
|
||||||
private decryptSecretsSettings(value: string, encryptionKey: string): ExternalSecretsSettings {
|
|
||||||
const decryptedData = AES.decrypt(value, encryptionKey);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(decryptedData.toString(enc.Utf8)) as ExternalSecretsSettings;
|
return jsonParse(decryptedData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'External Secrets Settings could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
|
'External Secrets Settings could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
|
||||||
|
@ -109,8 +104,7 @@ export class ExternalSecretsManager {
|
||||||
if (encryptedSettings === null) {
|
if (encryptedSettings === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const encryptionKey = await this.getEncryptionKey();
|
return this.decryptSecretsSettings(encryptedSettings);
|
||||||
return this.decryptSecretsSettings(encryptedSettings, encryptionKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalInit() {
|
private async internalInit() {
|
||||||
|
@ -327,13 +321,12 @@ export class ExternalSecretsManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptSecretsSettings(settings: ExternalSecretsSettings, encryptionKey: string): string {
|
private encryptSecretsSettings(settings: ExternalSecretsSettings): string {
|
||||||
return AES.encrypt(JSON.stringify(settings), encryptionKey).toString();
|
return this.cipher.encrypt(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveAndSetSettings(settings: ExternalSecretsSettings, settingsRepo: SettingsRepository) {
|
async saveAndSetSettings(settings: ExternalSecretsSettings, settingsRepo: SettingsRepository) {
|
||||||
const encryptionKey = await this.getEncryptionKey();
|
const encryptedSettings = this.encryptSecretsSettings(settings);
|
||||||
const encryptedSettings = this.encryptSecretsSettings(settings, encryptionKey);
|
|
||||||
await settingsRepo.saveEncryptedSecretsProviderSettings(encryptedSettings);
|
await settingsRepo.saveEncryptedSecretsProviderSettings(encryptedSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { ExecutionRepository } from '@db/repositories';
|
||||||
import { RoleService } from './services/role.service';
|
import { RoleService } from './services/role.service';
|
||||||
import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
|
import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
|
||||||
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
|
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
function userToPayload(user: User): {
|
function userToPayload(user: User): {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -50,22 +51,13 @@ function userToPayload(user: User): {
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class InternalHooks implements IInternalHooksClass {
|
export class InternalHooks implements IInternalHooksClass {
|
||||||
private instanceId: string;
|
|
||||||
|
|
||||||
public get telemetryInstanceId(): string {
|
|
||||||
return this.instanceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get telemetryInstance(): Telemetry {
|
|
||||||
return this.telemetry;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private telemetry: Telemetry,
|
private telemetry: Telemetry,
|
||||||
private nodeTypes: NodeTypes,
|
private nodeTypes: NodeTypes,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private executionRepository: ExecutionRepository,
|
private executionRepository: ExecutionRepository,
|
||||||
eventsService: EventsService,
|
eventsService: EventsService,
|
||||||
|
private readonly instanceSettings: InstanceSettings,
|
||||||
) {
|
) {
|
||||||
eventsService.on('telemetry.onFirstProductionWorkflowSuccess', async (metrics) =>
|
eventsService.on('telemetry.onFirstProductionWorkflowSuccess', async (metrics) =>
|
||||||
this.onFirstProductionWorkflowSuccess(metrics),
|
this.onFirstProductionWorkflowSuccess(metrics),
|
||||||
|
@ -75,9 +67,7 @@ export class InternalHooks implements IInternalHooksClass {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(instanceId: string) {
|
async init() {
|
||||||
this.instanceId = instanceId;
|
|
||||||
this.telemetry.setInstanceId(instanceId);
|
|
||||||
await this.telemetry.init();
|
await this.telemetry.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -813,7 +803,7 @@ export class InternalHooks implements IInternalHooksClass {
|
||||||
user_id: userCreatedCredentialsData.user.id,
|
user_id: userCreatedCredentialsData.user.id,
|
||||||
credential_type: userCreatedCredentialsData.credential_type,
|
credential_type: userCreatedCredentialsData.credential_type,
|
||||||
credential_id: userCreatedCredentialsData.credential_id,
|
credential_id: userCreatedCredentialsData.credential_id,
|
||||||
instance_id: this.instanceId,
|
instance_id: this.instanceSettings.instanceId,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -847,7 +837,7 @@ export class InternalHooks implements IInternalHooksClass {
|
||||||
user_id_sharer: userSharedCredentialsData.user_id_sharer,
|
user_id_sharer: userSharedCredentialsData.user_id_sharer,
|
||||||
user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added,
|
user_ids_sharees_added: userSharedCredentialsData.user_ids_sharees_added,
|
||||||
sharees_removed: userSharedCredentialsData.sharees_removed,
|
sharees_removed: userSharedCredentialsData.sharees_removed,
|
||||||
instance_id: this.instanceId,
|
instance_id: this.instanceSettings.instanceId,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
import { AES, enc } from 'crypto-js';
|
|
||||||
import type { Entry as LdapUser } from 'ldapts';
|
import type { Entry as LdapUser } from 'ldapts';
|
||||||
import { Filter } from 'ldapts/filters/Filter';
|
import { Filter } from 'ldapts/filters/Filter';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Cipher } from 'n8n-core';
|
||||||
import { validate } from 'jsonschema';
|
import { validate } from 'jsonschema';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
@ -110,22 +109,6 @@ export const validateLdapConfigurationSchema = (
|
||||||
return { valid, message };
|
return { valid, message };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt password using the instance's encryption key
|
|
||||||
*/
|
|
||||||
export const encryptPassword = async (password: string): Promise<string> => {
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
return AES.encrypt(password, encryptionKey).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt password using the instance's encryption key
|
|
||||||
*/
|
|
||||||
export const decryptPassword = async (password: string): Promise<string> => {
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
return AES.decrypt(password, encryptionKey).toString(enc.Utf8);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the LDAP configuration (decrypted) form the database
|
* Retrieve the LDAP configuration (decrypted) form the database
|
||||||
*/
|
*/
|
||||||
|
@ -134,7 +117,7 @@ export const getLdapConfig = async (): Promise<LdapConfig> => {
|
||||||
key: LDAP_FEATURE_NAME,
|
key: LDAP_FEATURE_NAME,
|
||||||
});
|
});
|
||||||
const configurationData = jsonParse<LdapConfig>(configuration.value);
|
const configurationData = jsonParse<LdapConfig>(configuration.value);
|
||||||
configurationData.bindingAdminPassword = await decryptPassword(
|
configurationData.bindingAdminPassword = Container.get(Cipher).decrypt(
|
||||||
configurationData.bindingAdminPassword,
|
configurationData.bindingAdminPassword,
|
||||||
);
|
);
|
||||||
return configurationData;
|
return configurationData;
|
||||||
|
@ -173,7 +156,7 @@ export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> =>
|
||||||
|
|
||||||
LdapManager.updateConfig({ ...ldapConfig });
|
LdapManager.updateConfig({ ...ldapConfig });
|
||||||
|
|
||||||
ldapConfig.bindingAdminPassword = await encryptPassword(ldapConfig.bindingAdminPassword);
|
ldapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(ldapConfig.bindingAdminPassword);
|
||||||
|
|
||||||
if (!ldapConfig.loginEnabled) {
|
if (!ldapConfig.loginEnabled) {
|
||||||
ldapConfig.synchronizationEnabled = false;
|
ldapConfig.synchronizationEnabled = false;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import Container, { Service } from 'typedi';
|
||||||
import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces';
|
import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces';
|
||||||
import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher';
|
import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher';
|
||||||
import { RedisService } from './services/redis.service';
|
import { RedisService } from './services/redis.service';
|
||||||
import { ObjectStoreService } from 'n8n-core';
|
import { InstanceSettings, ObjectStoreService } from 'n8n-core';
|
||||||
|
|
||||||
type FeatureReturnType = Partial<
|
type FeatureReturnType = Partial<
|
||||||
{
|
{
|
||||||
|
@ -29,20 +29,17 @@ export class License {
|
||||||
|
|
||||||
private manager: LicenseManager | undefined;
|
private manager: LicenseManager | undefined;
|
||||||
|
|
||||||
instanceId: string | undefined;
|
|
||||||
|
|
||||||
private redisPublisher: RedisServicePubSubPublisher;
|
private redisPublisher: RedisServicePubSubPublisher;
|
||||||
|
|
||||||
constructor() {
|
constructor(private readonly instanceSettings: InstanceSettings) {
|
||||||
this.logger = getLogger();
|
this.logger = getLogger();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(instanceId: string, instanceType: N8nInstanceType = 'main') {
|
async init(instanceType: N8nInstanceType = 'main') {
|
||||||
if (this.manager) {
|
if (this.manager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.instanceId = instanceId;
|
|
||||||
const isMainInstance = instanceType === 'main';
|
const isMainInstance = instanceType === 'main';
|
||||||
const server = config.getEnv('license.serverUrl');
|
const server = config.getEnv('license.serverUrl');
|
||||||
const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled');
|
const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled');
|
||||||
|
@ -67,7 +64,7 @@ export class License {
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
loadCertStr: async () => this.loadCertStr(),
|
loadCertStr: async () => this.loadCertStr(),
|
||||||
saveCertStr,
|
saveCertStr,
|
||||||
deviceFingerprint: () => instanceId,
|
deviceFingerprint: () => this.instanceSettings.instanceId,
|
||||||
onFeatureChange,
|
onFeatureChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import fsPromises from 'fs/promises';
|
||||||
import type { DirectoryLoader, Types } from 'n8n-core';
|
import type { DirectoryLoader, Types } from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
CUSTOM_EXTENSION_ENV,
|
CUSTOM_EXTENSION_ENV,
|
||||||
UserSettings,
|
InstanceSettings,
|
||||||
CustomDirectoryLoader,
|
CustomDirectoryLoader,
|
||||||
PackageDirectoryLoader,
|
PackageDirectoryLoader,
|
||||||
LazyPackageDirectoryLoader,
|
LazyPackageDirectoryLoader,
|
||||||
|
@ -47,10 +47,10 @@ export class LoadNodesAndCredentials {
|
||||||
|
|
||||||
includeNodes = config.getEnv('nodes.include');
|
includeNodes = config.getEnv('nodes.include');
|
||||||
|
|
||||||
private downloadFolder: string;
|
|
||||||
|
|
||||||
private postProcessors: Array<() => Promise<void>> = [];
|
private postProcessors: Array<() => Promise<void>> = [];
|
||||||
|
|
||||||
|
constructor(private readonly instanceSettings: InstanceSettings) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (inTest) throw new Error('Not available in tests');
|
if (inTest) throw new Error('Not available in tests');
|
||||||
|
|
||||||
|
@ -67,8 +67,6 @@ export class LoadNodesAndCredentials {
|
||||||
this.excludeNodes.push('n8n-nodes-base.e2eTest');
|
this.excludeNodes.push('n8n-nodes-base.e2eTest');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
|
||||||
|
|
||||||
// Load nodes from `n8n-nodes-base`
|
// Load nodes from `n8n-nodes-base`
|
||||||
const basePathsToScan = [
|
const basePathsToScan = [
|
||||||
// In case "n8n" package is in same node_modules folder.
|
// In case "n8n" package is in same node_modules folder.
|
||||||
|
@ -84,7 +82,9 @@ export class LoadNodesAndCredentials {
|
||||||
|
|
||||||
// Load nodes from any other `n8n-nodes-*` packages in the download directory
|
// Load nodes from any other `n8n-nodes-*` packages in the download directory
|
||||||
// This includes the community nodes
|
// This includes the community nodes
|
||||||
await this.loadNodesFromNodeModules(path.join(this.downloadFolder, 'node_modules'));
|
await this.loadNodesFromNodeModules(
|
||||||
|
path.join(this.instanceSettings.nodesDownloadDir, 'node_modules'),
|
||||||
|
);
|
||||||
|
|
||||||
await this.loadNodesFromCustomDirectories();
|
await this.loadNodesFromCustomDirectories();
|
||||||
await this.postProcessLoaders();
|
await this.postProcessLoaders();
|
||||||
|
@ -155,7 +155,7 @@ export class LoadNodesAndCredentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCustomDirectories(): string[] {
|
getCustomDirectories(): string[] {
|
||||||
const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()];
|
const customDirectories = [this.instanceSettings.customExtensionDir];
|
||||||
|
|
||||||
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
||||||
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV].split(';');
|
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV].split(';');
|
||||||
|
@ -172,7 +172,11 @@ export class LoadNodesAndCredentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPackage(packageName: string) {
|
async loadPackage(packageName: string) {
|
||||||
const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName);
|
const finalNodeUnpackedPath = path.join(
|
||||||
|
this.instanceSettings.nodesDownloadDir,
|
||||||
|
'node_modules',
|
||||||
|
packageName,
|
||||||
|
);
|
||||||
return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { AES, enc } from 'crypto-js';
|
|
||||||
import { TOTPService } from './totp.service';
|
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { UserRepository } from '@/databases/repositories';
|
import { Cipher } from 'n8n-core';
|
||||||
|
import { UserRepository } from '@db/repositories';
|
||||||
|
import { TOTPService } from './totp.service';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MfaService {
|
export class MfaService {
|
||||||
constructor(
|
constructor(
|
||||||
private userRepository: UserRepository,
|
private userRepository: UserRepository,
|
||||||
public totp: TOTPService,
|
public totp: TOTPService,
|
||||||
private encryptionKey: string,
|
private cipher: Cipher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public generateRecoveryCodes(n = 10) {
|
public generateRecoveryCodes(n = 10) {
|
||||||
|
@ -17,9 +17,7 @@ export class MfaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateEncryptedRecoveryCodes() {
|
public generateEncryptedRecoveryCodes() {
|
||||||
return this.generateRecoveryCodes().map((code) =>
|
return this.generateRecoveryCodes().map((code) => this.cipher.encrypt(code));
|
||||||
AES.encrypt(code, this.encryptionKey).toString(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveSecretAndRecoveryCodes(userId: string, secret: string, recoveryCodes: string[]) {
|
public async saveSecretAndRecoveryCodes(userId: string, secret: string, recoveryCodes: string[]) {
|
||||||
|
@ -34,10 +32,8 @@ export class MfaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public encryptSecretAndRecoveryCodes(rawSecret: string, rawRecoveryCodes: string[]) {
|
public encryptSecretAndRecoveryCodes(rawSecret: string, rawRecoveryCodes: string[]) {
|
||||||
const encryptedSecret = AES.encrypt(rawSecret, this.encryptionKey).toString(),
|
const encryptedSecret = this.cipher.encrypt(rawSecret),
|
||||||
encryptedRecoveryCodes = rawRecoveryCodes.map((code) =>
|
encryptedRecoveryCodes = rawRecoveryCodes.map((code) => this.cipher.encrypt(code));
|
||||||
AES.encrypt(code, this.encryptionKey).toString(),
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
encryptedRecoveryCodes,
|
encryptedRecoveryCodes,
|
||||||
encryptedSecret,
|
encryptedSecret,
|
||||||
|
@ -46,10 +42,8 @@ export class MfaService {
|
||||||
|
|
||||||
private decryptSecretAndRecoveryCodes(mfaSecret: string, mfaRecoveryCodes: string[]) {
|
private decryptSecretAndRecoveryCodes(mfaSecret: string, mfaRecoveryCodes: string[]) {
|
||||||
return {
|
return {
|
||||||
decryptedSecret: AES.decrypt(mfaSecret, this.encryptionKey).toString(enc.Utf8),
|
decryptedSecret: this.cipher.decrypt(mfaSecret),
|
||||||
decryptedRecoveryCodes: mfaRecoveryCodes.map((code) =>
|
decryptedRecoveryCodes: mfaRecoveryCodes.map((code) => this.cipher.decrypt(code)),
|
||||||
AES.decrypt(code, this.encryptionKey).toString(enc.Utf8),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +60,7 @@ export class MfaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public encryptRecoveryCodes(mfaRecoveryCodes: string[]) {
|
public encryptRecoveryCodes(mfaRecoveryCodes: string[]) {
|
||||||
return mfaRecoveryCodes.map((code) => AES.encrypt(code, this.encryptionKey).toString());
|
return mfaRecoveryCodes.map((code) => this.cipher.encrypt(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disableMfa(userId: string) {
|
public async disableMfa(userId: string) {
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import OTPAuth from 'otpauth';
|
import OTPAuth from 'otpauth';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class TOTPService {
|
export class TOTPService {
|
||||||
generateSecret(): string {
|
generateSecret(): string {
|
||||||
return new OTPAuth.Secret()?.base32;
|
return new OTPAuth.Secret()?.base32;
|
||||||
|
|
|
@ -93,7 +93,7 @@ export = {
|
||||||
return res.status(404).json({ message: 'Not Found' });
|
return res.status(404).json({ message: 'Not Found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = new CredentialsHelper('')
|
const schema = Container.get(CredentialsHelper)
|
||||||
.getCredentialsProperties(credentialTypeName)
|
.getCredentialsProperties(credentialTypeName)
|
||||||
.filter((property) => property.type !== 'hidden');
|
.filter((property) => property.type !== 'hidden');
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const validCredentialsProperties = (
|
||||||
): express.Response | void => {
|
): express.Response | void => {
|
||||||
const { type, data } = req.body;
|
const { type, data } = req.body;
|
||||||
|
|
||||||
const properties = new CredentialsHelper('')
|
const properties = Container.get(CredentialsHelper)
|
||||||
.getCredentialsProperties(type)
|
.getCredentialsProperties(type)
|
||||||
.filter((property) => property.type !== 'hidden');
|
.filter((property) => property.type !== 'hidden');
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { UserSettings, Credentials } from 'n8n-core';
|
import { Credentials } from 'n8n-core';
|
||||||
import type { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
|
import type { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import type { ICredentialsDb } from '@/Interfaces';
|
import type { ICredentialsDb } from '@/Interfaces';
|
||||||
|
@ -87,8 +87,6 @@ export async function removeCredential(credentials: CredentialsEntity): Promise<
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> {
|
export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
// Encrypt the data
|
// Encrypt the data
|
||||||
const coreCredential = new Credentials(
|
const coreCredential = new Credentials(
|
||||||
{ id: null, name: credential.name },
|
{ id: null, name: credential.name },
|
||||||
|
@ -97,7 +95,7 @@ export async function encryptCredential(credential: CredentialsEntity): Promise<
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
coreCredential.setData(credential.data, encryptionKey);
|
coreCredential.setData(credential.data);
|
||||||
|
|
||||||
return coreCredential.getDataToSave() as ICredentialsDb;
|
return coreCredential.getDataToSave() as ICredentialsDb;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ import {
|
||||||
LoadMappingOptions,
|
LoadMappingOptions,
|
||||||
LoadNodeParameterOptions,
|
LoadNodeParameterOptions,
|
||||||
LoadNodeListSearch,
|
LoadNodeListSearch,
|
||||||
UserSettings,
|
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -146,7 +145,6 @@ import { SourceControlService } from '@/environments/sourceControl/sourceControl
|
||||||
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
|
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
|
||||||
import { ExecutionRepository, SettingsRepository } from '@db/repositories';
|
import { ExecutionRepository, SettingsRepository } from '@db/repositories';
|
||||||
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
||||||
import { TOTPService } from './Mfa/totp.service';
|
|
||||||
import { MfaService } from './Mfa/mfa.service';
|
import { MfaService } from './Mfa/mfa.service';
|
||||||
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
|
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
|
||||||
import type { FrontendService } from './services/frontend.service';
|
import type { FrontendService } from './services/frontend.service';
|
||||||
|
@ -159,25 +157,25 @@ import { WorkflowHistoryController } from './workflows/workflowHistory/workflowH
|
||||||
const exec = promisify(callbackExec);
|
const exec = promisify(callbackExec);
|
||||||
|
|
||||||
export class Server extends AbstractServer {
|
export class Server extends AbstractServer {
|
||||||
endpointPresetCredentials: string;
|
private endpointPresetCredentials: string;
|
||||||
|
|
||||||
waitTracker: WaitTracker;
|
private waitTracker: WaitTracker;
|
||||||
|
|
||||||
activeExecutionsInstance: ActiveExecutions;
|
private activeExecutionsInstance: ActiveExecutions;
|
||||||
|
|
||||||
presetCredentialsLoaded: boolean;
|
private presetCredentialsLoaded: boolean;
|
||||||
|
|
||||||
loadNodesAndCredentials: LoadNodesAndCredentials;
|
private loadNodesAndCredentials: LoadNodesAndCredentials;
|
||||||
|
|
||||||
nodeTypes: NodeTypes;
|
private nodeTypes: NodeTypes;
|
||||||
|
|
||||||
credentialTypes: ICredentialTypes;
|
private credentialTypes: ICredentialTypes;
|
||||||
|
|
||||||
frontendService: FrontendService;
|
private frontendService: FrontendService;
|
||||||
|
|
||||||
postHog: PostHogClient;
|
private postHog: PostHogClient;
|
||||||
|
|
||||||
push: Push;
|
private push: Push;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('main');
|
super('main');
|
||||||
|
@ -285,15 +283,13 @@ export class Server extends AbstractServer {
|
||||||
const repositories = Db.collections;
|
const repositories = Db.collections;
|
||||||
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
|
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
const logger = LoggerProxy;
|
const logger = LoggerProxy;
|
||||||
const internalHooks = Container.get(InternalHooks);
|
const internalHooks = Container.get(InternalHooks);
|
||||||
const mailer = Container.get(UserManagementMailer);
|
const mailer = Container.get(UserManagementMailer);
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
const jwtService = Container.get(JwtService);
|
const jwtService = Container.get(JwtService);
|
||||||
const postHog = this.postHog;
|
const postHog = this.postHog;
|
||||||
const mfaService = new MfaService(repositories.User, new TOTPService(), encryptionKey);
|
const mfaService = Container.get(MfaService);
|
||||||
|
|
||||||
const controllers: object[] = [
|
const controllers: object[] = [
|
||||||
new EventBusController(),
|
new EventBusController(),
|
||||||
|
@ -376,19 +372,16 @@ export class Server extends AbstractServer {
|
||||||
await Container.get(MetricsService).configureMetrics(this.app);
|
await Container.get(MetricsService).configureMetrics(this.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.instanceId = await UserSettings.getInstanceId();
|
|
||||||
|
|
||||||
this.frontendService.addToSettings({
|
this.frontendService.addToSettings({
|
||||||
isNpmAvailable: await exec('npm --version')
|
isNpmAvailable: await exec('npm --version')
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false),
|
.catch(() => false),
|
||||||
versionCli: N8N_VERSION,
|
versionCli: N8N_VERSION,
|
||||||
instanceId: this.instanceId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.externalHooks.run('frontend.settings', [this.frontendService.getSettings()]);
|
await this.externalHooks.run('frontend.settings', [this.frontendService.getSettings()]);
|
||||||
|
|
||||||
await this.postHog.init(this.instanceId);
|
await this.postHog.init();
|
||||||
|
|
||||||
const publicApiEndpoint = config.getEnv('publicApi.path');
|
const publicApiEndpoint = config.getEnv('publicApi.path');
|
||||||
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
||||||
|
@ -739,18 +732,11 @@ export class Server extends AbstractServer {
|
||||||
throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL);
|
throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
let encryptionKey: string;
|
|
||||||
try {
|
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
} catch (error) {
|
|
||||||
throw new ResponseHelper.InternalServerError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const timezone = config.getEnv('generic.timezone');
|
const timezone = config.getEnv('generic.timezone');
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = Container.get(CredentialsHelper);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
additionalData,
|
additionalData,
|
||||||
credential as INodeCredentialsDetails,
|
credential as INodeCredentialsDetails,
|
||||||
|
@ -835,7 +821,7 @@ export class Server extends AbstractServer {
|
||||||
credential.nodesAccess,
|
credential.nodesAccess,
|
||||||
);
|
);
|
||||||
|
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
|
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
|
@ -889,18 +875,11 @@ export class Server extends AbstractServer {
|
||||||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
let encryptionKey: string;
|
|
||||||
try {
|
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
} catch (error) {
|
|
||||||
throw new ResponseHelper.InternalServerError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const timezone = config.getEnv('generic.timezone');
|
const timezone = config.getEnv('generic.timezone');
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = Container.get(CredentialsHelper);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
additionalData,
|
additionalData,
|
||||||
credential as INodeCredentialsDetails,
|
credential as INodeCredentialsDetails,
|
||||||
|
@ -952,7 +931,7 @@ export class Server extends AbstractServer {
|
||||||
credential.type,
|
credential.type,
|
||||||
credential.nodesAccess,
|
credential.nodesAccess,
|
||||||
);
|
);
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
newCredentialsData.updatedAt = new Date();
|
newCredentialsData.updatedAt = new Date();
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
|
|
||||||
/* eslint-disable id-denylist */
|
/* eslint-disable id-denylist */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { UserSettings, WorkflowExecute } from 'n8n-core';
|
import { WorkflowExecute } from 'n8n-core';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
@ -1033,14 +1029,10 @@ export async function getBase(
|
||||||
const webhookWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookWaiting');
|
const webhookWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookWaiting');
|
||||||
const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest');
|
const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest');
|
||||||
|
|
||||||
const [encryptionKey, variables] = await Promise.all([
|
const variables = await WorkflowHelpers.getVariables();
|
||||||
UserSettings.getEncryptionKey(),
|
|
||||||
WorkflowHelpers.getVariables(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credentialsHelper: new CredentialsHelper(encryptionKey),
|
credentialsHelper: Container.get(CredentialsHelper),
|
||||||
encryptionKey,
|
|
||||||
executeWorkflow,
|
executeWorkflow,
|
||||||
restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'),
|
restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'),
|
||||||
timezone,
|
timezone,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { setDefaultResultOrder } from 'dns';
|
||||||
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import type { IProcessMessage } from 'n8n-core';
|
import type { IProcessMessage } from 'n8n-core';
|
||||||
import { BinaryDataService, UserSettings, WorkflowExecute } from 'n8n-core';
|
import { BinaryDataService, WorkflowExecute } from 'n8n-core';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
|
@ -107,8 +107,6 @@ class WorkflowRunnerProcess {
|
||||||
// Init db since we need to read the license.
|
// Init db since we need to read the license.
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
|
||||||
const userSettings = await UserSettings.prepareUserSettings();
|
|
||||||
|
|
||||||
const loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
const loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
||||||
await loadNodesAndCredentials.init();
|
await loadNodesAndCredentials.init();
|
||||||
|
|
||||||
|
@ -118,15 +116,14 @@ class WorkflowRunnerProcess {
|
||||||
const externalHooks = Container.get(ExternalHooks);
|
const externalHooks = Container.get(ExternalHooks);
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
const instanceId = userSettings.instanceId ?? '';
|
await Container.get(PostHogClient).init();
|
||||||
await Container.get(PostHogClient).init(instanceId);
|
await Container.get(InternalHooks).init();
|
||||||
await Container.get(InternalHooks).init(instanceId);
|
|
||||||
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
await Container.get(BinaryDataService).init(binaryDataConfig);
|
await Container.get(BinaryDataService).init(binaryDataConfig);
|
||||||
|
|
||||||
const license = Container.get(License);
|
const license = Container.get(License);
|
||||||
await license.init(instanceId);
|
await license.init();
|
||||||
|
|
||||||
const workflowSettings = this.data.workflowData.settings ?? {};
|
const workflowSettings = this.data.workflowData.settings ?? {};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Container } from 'typedi';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { toFlaggedNode } from '@/audit/utils';
|
import { toFlaggedNode } from '@/audit/utils';
|
||||||
import { separate } from '@/utils';
|
import { separate } from '@/utils';
|
||||||
|
@ -81,7 +82,7 @@ function getUnprotectedWebhookNodes(workflows: WorkflowEntity[]) {
|
||||||
|
|
||||||
async function getNextVersions(currentVersionName: string) {
|
async function getNextVersions(currentVersionName: string) {
|
||||||
const BASE_URL = config.getEnv('versionNotifications.endpoint');
|
const BASE_URL = config.getEnv('versionNotifications.endpoint');
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const { instanceId } = Container.get(InstanceSettings);
|
||||||
|
|
||||||
const response = await axios.get<n8n.Version[]>(BASE_URL + currentVersionName, {
|
const response = await axios.get<n8n.Version[]>(BASE_URL + currentVersionName, {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
|
|
@ -2,8 +2,7 @@ import { Command } from '@oclif/command';
|
||||||
import { ExitError } from '@oclif/errors';
|
import { ExitError } from '@oclif/errors';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
|
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
|
||||||
import type { IUserSettings } from 'n8n-core';
|
import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
|
||||||
import { BinaryDataService, ObjectStoreService, UserSettings } from 'n8n-core';
|
|
||||||
import type { AbstractServer } from '@/AbstractServer';
|
import type { AbstractServer } from '@/AbstractServer';
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
@ -30,11 +29,9 @@ export abstract class BaseCommand extends Command {
|
||||||
|
|
||||||
protected nodeTypes: NodeTypes;
|
protected nodeTypes: NodeTypes;
|
||||||
|
|
||||||
protected userSettings: IUserSettings;
|
protected instanceSettings: InstanceSettings;
|
||||||
|
|
||||||
protected instanceId: string;
|
private instanceType: N8nInstanceType = 'main';
|
||||||
|
|
||||||
instanceType: N8nInstanceType = 'main';
|
|
||||||
|
|
||||||
queueModeId: string;
|
queueModeId: string;
|
||||||
|
|
||||||
|
@ -48,7 +45,7 @@ export abstract class BaseCommand extends Command {
|
||||||
process.once('SIGINT', async () => this.stopProcess());
|
process.once('SIGINT', async () => this.stopProcess());
|
||||||
|
|
||||||
// Make sure the settings exist
|
// Make sure the settings exist
|
||||||
this.userSettings = await UserSettings.prepareUserSettings();
|
this.instanceSettings = Container.get(InstanceSettings);
|
||||||
|
|
||||||
await Container.get(LoadNodesAndCredentials).init();
|
await Container.get(LoadNodesAndCredentials).init();
|
||||||
this.nodeTypes = Container.get(NodeTypes);
|
this.nodeTypes = Container.get(NodeTypes);
|
||||||
|
@ -76,9 +73,8 @@ export abstract class BaseCommand extends Command {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.instanceId = this.userSettings.instanceId ?? '';
|
await Container.get(PostHogClient).init();
|
||||||
await Container.get(PostHogClient).init(this.instanceId);
|
await Container.get(InternalHooks).init();
|
||||||
await Container.get(InternalHooks).init(this.instanceId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setInstanceType(instanceType: N8nInstanceType) {
|
protected setInstanceType(instanceType: N8nInstanceType) {
|
||||||
|
@ -241,7 +237,7 @@ export abstract class BaseCommand extends Command {
|
||||||
|
|
||||||
async initLicense(): Promise<void> {
|
async initLicense(): Promise<void> {
|
||||||
const license = Container.get(License);
|
const license = Container.get(License);
|
||||||
await license.init(this.instanceId, this.instanceType ?? 'main');
|
await license.init(this.instanceType ?? 'main');
|
||||||
|
|
||||||
const activationKey = config.getEnv('license.activationKey');
|
const activationKey = config.getEnv('license.activationKey');
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { flags } from '@oclif/command';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
import { Credentials, UserSettings } from 'n8n-core';
|
import { Credentials } from 'n8n-core';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces';
|
import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces';
|
||||||
import { BaseCommand } from '../BaseCommand';
|
import { BaseCommand } from '../BaseCommand';
|
||||||
|
@ -113,13 +113,11 @@ export class ExportCredentialsCommand extends BaseCommand {
|
||||||
const credentials: ICredentialsDb[] = await Db.collections.Credentials.findBy(findQuery);
|
const credentials: ICredentialsDb[] = await Db.collections.Credentials.findBy(findQuery);
|
||||||
|
|
||||||
if (flags.decrypted) {
|
if (flags.decrypted) {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
for (let i = 0; i < credentials.length; i++) {
|
for (let i = 0; i < credentials.length; i++) {
|
||||||
const { name, type, nodesAccess, data } = credentials[i];
|
const { name, type, nodesAccess, data } = credentials[i];
|
||||||
const id = credentials[i].id;
|
const id = credentials[i].id;
|
||||||
const credential = new Credentials({ id, name }, type, nodesAccess, data);
|
const credential = new Credentials({ id, name }, type, nodesAccess, data);
|
||||||
const plainData = credential.getData(encryptionKey);
|
const plainData = credential.getData();
|
||||||
(credentials[i] as ICredentialsDecryptedDb).data = plainData;
|
(credentials[i] as ICredentialsDecryptedDb).data = plainData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,8 +72,6 @@ export class ImportCredentialsCommand extends BaseCommand {
|
||||||
await this.initOwnerCredentialRole();
|
await this.initOwnerCredentialRole();
|
||||||
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
|
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
|
||||||
|
|
||||||
const encryptionKey = this.userSettings.encryptionKey;
|
|
||||||
|
|
||||||
if (flags.separate) {
|
if (flags.separate) {
|
||||||
let { input: inputPath } = flags;
|
let { input: inputPath } = flags;
|
||||||
|
|
||||||
|
@ -97,7 +95,7 @@ export class ImportCredentialsCommand extends BaseCommand {
|
||||||
|
|
||||||
if (typeof credential.data === 'object') {
|
if (typeof credential.data === 'object') {
|
||||||
// plain data / decrypted input. Should be encrypted first.
|
// plain data / decrypted input. Should be encrypted first.
|
||||||
Credentials.prototype.setData.call(credential, credential.data, encryptionKey);
|
Credentials.prototype.setData.call(credential, credential.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storeCredential(credential, user);
|
await this.storeCredential(credential, user);
|
||||||
|
@ -125,7 +123,7 @@ export class ImportCredentialsCommand extends BaseCommand {
|
||||||
for (const credential of credentials) {
|
for (const credential of credentials) {
|
||||||
if (typeof credential.data === 'object') {
|
if (typeof credential.data === 'object') {
|
||||||
// plain data / decrypted input. Should be encrypted first.
|
// plain data / decrypted input. Should be encrypted first.
|
||||||
Credentials.prototype.setData.call(credential, credential.data, encryptionKey);
|
Credentials.prototype.setData.call(credential, credential.data);
|
||||||
}
|
}
|
||||||
await this.storeCredential(credential, user);
|
await this.storeCredential(credential, user);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export class LicenseInfoCommand extends BaseCommand {
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
const license = Container.get(License);
|
const license = Container.get(License);
|
||||||
await license.init(this.instanceId);
|
await license.init();
|
||||||
|
|
||||||
this.logger.info('Printing license information:\n' + license.getInfo());
|
this.logger.info('Printing license information:\n' + license.getInfo());
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import path from 'path';
|
||||||
import { mkdir } from 'fs/promises';
|
import { mkdir } from 'fs/promises';
|
||||||
import { createReadStream, createWriteStream, existsSync } from 'fs';
|
import { createReadStream, createWriteStream, existsSync } from 'fs';
|
||||||
import localtunnel from 'localtunnel';
|
import localtunnel from 'localtunnel';
|
||||||
import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core';
|
|
||||||
import { flags } from '@oclif/command';
|
import { flags } from '@oclif/command';
|
||||||
import stream from 'stream';
|
import stream from 'stream';
|
||||||
import replaceStream from 'replacestream';
|
import replaceStream from 'replacestream';
|
||||||
|
@ -245,7 +244,7 @@ export class Start extends BaseCommand {
|
||||||
if (!config.getEnv('userManagement.jwtSecret')) {
|
if (!config.getEnv('userManagement.jwtSecret')) {
|
||||||
// If we don't have a JWT secret set, generate
|
// If we don't have a JWT secret set, generate
|
||||||
// one based and save to config.
|
// one based and save to config.
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
const { encryptionKey } = this.instanceSettings;
|
||||||
|
|
||||||
// For a key off every other letter from encryption key
|
// For a key off every other letter from encryption key
|
||||||
// CAREFUL: do not change this or it breaks all existing tokens.
|
// CAREFUL: do not change this or it breaks all existing tokens.
|
||||||
|
@ -256,8 +255,6 @@ export class Start extends BaseCommand {
|
||||||
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
|
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
// Load settings from database and set them to config.
|
// Load settings from database and set them to config.
|
||||||
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
|
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
|
||||||
databaseSettings.forEach((setting) => {
|
databaseSettings.forEach((setting) => {
|
||||||
|
@ -285,28 +282,19 @@ export class Start extends BaseCommand {
|
||||||
if (flags.tunnel) {
|
if (flags.tunnel) {
|
||||||
this.log('\nWaiting for tunnel ...');
|
this.log('\nWaiting for tunnel ...');
|
||||||
|
|
||||||
let tunnelSubdomain;
|
let tunnelSubdomain =
|
||||||
if (
|
process.env.N8N_TUNNEL_SUBDOMAIN ?? this.instanceSettings.tunnelSubdomain ?? '';
|
||||||
process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined &&
|
|
||||||
process.env[TUNNEL_SUBDOMAIN_ENV] !== ''
|
|
||||||
) {
|
|
||||||
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
|
|
||||||
} else if (this.userSettings.tunnelSubdomain !== undefined) {
|
|
||||||
tunnelSubdomain = this.userSettings.tunnelSubdomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tunnelSubdomain === undefined) {
|
if (tunnelSubdomain === '') {
|
||||||
// When no tunnel subdomain did exist yet create a new random one
|
// When no tunnel subdomain did exist yet create a new random one
|
||||||
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
this.userSettings.tunnelSubdomain = Array.from({ length: 24 })
|
tunnelSubdomain = Array.from({ length: 24 })
|
||||||
.map(() => {
|
.map(() =>
|
||||||
return availableCharacters.charAt(
|
availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length)),
|
||||||
Math.floor(Math.random() * availableCharacters.length),
|
)
|
||||||
);
|
|
||||||
})
|
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
await UserSettings.writeUserSettings(this.userSettings);
|
this.instanceSettings.update({ tunnelSubdomain });
|
||||||
}
|
}
|
||||||
|
|
||||||
const tunnelSettings: localtunnel.TunnelConfig = {
|
const tunnelSettings: localtunnel.TunnelConfig = {
|
||||||
|
|
|
@ -318,7 +318,6 @@ export class Worker extends BaseCommand {
|
||||||
await Container.get(OrchestrationWorkerService).init();
|
await Container.get(OrchestrationWorkerService).init();
|
||||||
await Container.get(OrchestrationHandlerWorkerService).initWithOptions({
|
await Container.get(OrchestrationHandlerWorkerService).initWithOptions({
|
||||||
queueModeId: this.queueModeId,
|
queueModeId: this.queueModeId,
|
||||||
instanceId: this.instanceId,
|
|
||||||
redisPublisher: Container.get(OrchestrationWorkerService).redisPublisher,
|
redisPublisher: Container.get(OrchestrationWorkerService).redisPublisher,
|
||||||
getRunningJobIds: () => Object.keys(Worker.runningJobs),
|
getRunningJobIds: () => Object.keys(Worker.runningJobs),
|
||||||
getRunningJobsSummary: () => Object.values(Worker.runningJobsSummary),
|
getRunningJobsSummary: () => Object.values(Worker.runningJobsSummary),
|
||||||
|
|
|
@ -21,12 +21,9 @@ if (inE2ETests) {
|
||||||
N8N_AI_ENABLED: 'true',
|
N8N_AI_ENABLED: 'true',
|
||||||
};
|
};
|
||||||
} else if (inTest) {
|
} else if (inTest) {
|
||||||
const testsDir = join(tmpdir(), 'n8n-tests/');
|
|
||||||
mkdirSync(testsDir, { recursive: true });
|
|
||||||
process.env.N8N_LOG_LEVEL = 'silent';
|
process.env.N8N_LOG_LEVEL = 'silent';
|
||||||
process.env.N8N_ENCRYPTION_KEY = 'test-encryption-key';
|
process.env.N8N_ENCRYPTION_KEY = 'test-encryption-key';
|
||||||
process.env.N8N_PUBLIC_API_DISABLED = 'true';
|
process.env.N8N_PUBLIC_API_DISABLED = 'true';
|
||||||
process.env.N8N_USER_FOLDER = mkdtempSync(testsDir);
|
|
||||||
process.env.SKIP_STATISTICS_EVENTS = 'true';
|
process.env.SKIP_STATISTICS_EVENTS = 'true';
|
||||||
} else {
|
} else {
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import convict from 'convict';
|
import convict from 'convict';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Container } from 'typedi';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import { jsonParse } from 'n8n-workflow';
|
import { jsonParse } from 'n8n-workflow';
|
||||||
import { ensureStringArray } from './utils';
|
import { ensureStringArray } from './utils';
|
||||||
|
|
||||||
|
@ -881,7 +882,7 @@ export const schema = {
|
||||||
location: {
|
location: {
|
||||||
doc: 'Log file location; only used if log output is set to file.',
|
doc: 'Log file location; only used if log output is set to file.',
|
||||||
format: String,
|
format: String,
|
||||||
default: path.join(UserSettings.getUserN8nFolderPath(), 'logs/n8n.log'),
|
default: path.join(Container.get(InstanceSettings).n8nFolder, 'logs/n8n.log'),
|
||||||
env: 'N8N_LOG_FILE_LOCATION',
|
env: 'N8N_LOG_FILE_LOCATION',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -947,7 +948,7 @@ export const schema = {
|
||||||
},
|
},
|
||||||
localStoragePath: {
|
localStoragePath: {
|
||||||
format: String,
|
format: String,
|
||||||
default: path.join(UserSettings.getUserN8nFolderPath(), 'binaryData'),
|
default: path.join(Container.get(InstanceSettings).n8nFolder, 'binaryData'),
|
||||||
env: 'N8N_BINARY_DATA_STORAGE_PATH',
|
env: 'N8N_BINARY_DATA_STORAGE_PATH',
|
||||||
doc: 'Path for binary data storage in "filesystem" mode',
|
doc: 'Path for binary data storage in "filesystem" mode',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { resolve, join, dirname } from 'path';
|
import { resolve, join, dirname } from 'path';
|
||||||
|
import { Container } from 'typedi';
|
||||||
import type { n8n } from 'n8n-core';
|
import type { n8n } from 'n8n-core';
|
||||||
import { RESPONSE_ERROR_MESSAGES as CORE_RESPONSE_ERROR_MESSAGES, UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import { jsonParse } from 'n8n-workflow';
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
|
||||||
const { NODE_ENV, E2E_TESTS } = process.env;
|
const { NODE_ENV, E2E_TESTS } = process.env;
|
||||||
|
@ -16,7 +17,10 @@ export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__';
|
||||||
export const CLI_DIR = resolve(__dirname, '..');
|
export const CLI_DIR = resolve(__dirname, '..');
|
||||||
export const TEMPLATES_DIR = join(CLI_DIR, 'templates');
|
export const TEMPLATES_DIR = join(CLI_DIR, 'templates');
|
||||||
export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base'));
|
export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base'));
|
||||||
export const GENERATED_STATIC_DIR = join(UserSettings.getUserHome(), '.cache/n8n/public');
|
export const GENERATED_STATIC_DIR = join(
|
||||||
|
Container.get(InstanceSettings).userHome,
|
||||||
|
'.cache/n8n/public',
|
||||||
|
);
|
||||||
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
||||||
|
|
||||||
export function getN8nPackageJson() {
|
export function getN8nPackageJson() {
|
||||||
|
@ -34,7 +38,6 @@ export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
|
||||||
export const RESPONSE_ERROR_MESSAGES = {
|
export const RESPONSE_ERROR_MESSAGES = {
|
||||||
NO_CREDENTIAL: 'Credential not found',
|
NO_CREDENTIAL: 'Credential not found',
|
||||||
NO_NODE: 'Node not found',
|
NO_NODE: 'Node not found',
|
||||||
NO_ENCRYPTION_KEY: CORE_RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
|
||||||
PACKAGE_NAME_NOT_PROVIDED: 'Package name is required',
|
PACKAGE_NAME_NOT_PROVIDED: 'Package name is required',
|
||||||
PACKAGE_NAME_NOT_VALID: `Package name is not valid - it must start with "${NODE_PACKAGE_PREFIX}"`,
|
PACKAGE_NAME_NOT_VALID: `Package name is not valid - it must start with "${NODE_PACKAGE_PREFIX}"`,
|
||||||
PACKAGE_NOT_INSTALLED: 'This package is not installed - you must install it first',
|
PACKAGE_NOT_INSTALLED: 'This package is not installed - you must install it first',
|
||||||
|
|
|
@ -12,9 +12,7 @@ import { LICENSE_FEATURES, inE2ETests } from '@/constants';
|
||||||
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
|
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
|
||||||
import type { UserSetupPayload } from '@/requests';
|
import type { UserSetupPayload } from '@/requests';
|
||||||
import type { BooleanLicenseFeature } from '@/Interfaces';
|
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import { MfaService } from '@/Mfa/mfa.service';
|
import { MfaService } from '@/Mfa/mfa.service';
|
||||||
import { TOTPService } from '@/Mfa/totp.service';
|
|
||||||
|
|
||||||
if (!inE2ETests) {
|
if (!inE2ETests) {
|
||||||
console.error('E2E endpoints only allowed during E2E tests');
|
console.error('E2E endpoints only allowed during E2E tests');
|
||||||
|
@ -77,6 +75,7 @@ export class E2EController {
|
||||||
private settingsRepo: SettingsRepository,
|
private settingsRepo: SettingsRepository,
|
||||||
private userRepo: UserRepository,
|
private userRepo: UserRepository,
|
||||||
private workflowRunner: ActiveWorkflowRunner,
|
private workflowRunner: ActiveWorkflowRunner,
|
||||||
|
private mfaService: MfaService,
|
||||||
) {
|
) {
|
||||||
license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
|
license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
|
||||||
this.enabledFeatures[feature] ?? false;
|
this.enabledFeatures[feature] ?? false;
|
||||||
|
@ -141,10 +140,6 @@ export class E2EController {
|
||||||
roles.map(([name, scope], index) => ({ name, scope, id: (index + 1).toString() })),
|
roles.map(([name, scope], index) => ({ name, scope, id: (index + 1).toString() })),
|
||||||
);
|
);
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
const mfaService = new MfaService(this.userRepo, new TOTPService(), encryptionKey);
|
|
||||||
|
|
||||||
const instanceOwner = {
|
const instanceOwner = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
...owner,
|
...owner,
|
||||||
|
@ -153,10 +148,8 @@ export class E2EController {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) {
|
if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) {
|
||||||
const { encryptedRecoveryCodes, encryptedSecret } = mfaService.encryptSecretAndRecoveryCodes(
|
const { encryptedRecoveryCodes, encryptedSecret } =
|
||||||
owner.mfaSecret,
|
this.mfaService.encryptSecretAndRecoveryCodes(owner.mfaSecret, owner.mfaRecoveryCodes);
|
||||||
owner.mfaRecoveryCodes,
|
|
||||||
);
|
|
||||||
instanceOwner.mfaSecret = encryptedSecret;
|
instanceOwner.mfaSecret = encryptedSecret;
|
||||||
instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes;
|
instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,11 +61,7 @@ EECredentialsController.get(
|
||||||
|
|
||||||
const { data: _, ...rest } = credential;
|
const { data: _, ...rest } = credential;
|
||||||
|
|
||||||
const key = await EECredentials.getEncryptionKey();
|
const decryptedData = EECredentials.redact(EECredentials.decrypt(credential), credential);
|
||||||
const decryptedData = EECredentials.redact(
|
|
||||||
await EECredentials.decrypt(key, credential),
|
|
||||||
credential,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { data: decryptedData, ...rest };
|
return { data: decryptedData, ...rest };
|
||||||
}),
|
}),
|
||||||
|
@ -81,8 +77,6 @@ EECredentialsController.post(
|
||||||
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
|
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
|
||||||
const { credentials } = req.body;
|
const { credentials } = req.body;
|
||||||
|
|
||||||
const encryptionKey = await EECredentials.getEncryptionKey();
|
|
||||||
|
|
||||||
const credentialId = credentials.id;
|
const credentialId = credentials.id;
|
||||||
const { ownsCredential } = await EECredentials.isOwned(req.user, credentialId);
|
const { ownsCredential } = await EECredentials.isOwned(req.user, credentialId);
|
||||||
|
|
||||||
|
@ -92,17 +86,17 @@ EECredentialsController.post(
|
||||||
throw new ResponseHelper.UnauthorizedError('Forbidden');
|
throw new ResponseHelper.UnauthorizedError('Forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedData = await EECredentials.decrypt(encryptionKey, sharing.credentials);
|
const decryptedData = EECredentials.decrypt(sharing.credentials);
|
||||||
Object.assign(credentials, { data: decryptedData });
|
Object.assign(credentials, { data: decryptedData });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedCredentials = deepCopy(credentials);
|
const mergedCredentials = deepCopy(credentials);
|
||||||
if (mergedCredentials.data && sharing?.credentials) {
|
if (mergedCredentials.data && sharing?.credentials) {
|
||||||
const decryptedData = await EECredentials.decrypt(encryptionKey, sharing.credentials);
|
const decryptedData = EECredentials.decrypt(sharing.credentials);
|
||||||
mergedCredentials.data = EECredentials.unredact(mergedCredentials.data, decryptedData);
|
mergedCredentials.data = EECredentials.unredact(mergedCredentials.data, decryptedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return EECredentials.test(req.user, encryptionKey, mergedCredentials);
|
return EECredentials.test(req.user, mergedCredentials);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -86,9 +86,8 @@ credentialsController.get(
|
||||||
return { ...rest };
|
return { ...rest };
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = await CredentialsService.getEncryptionKey();
|
|
||||||
const decryptedData = CredentialsService.redact(
|
const decryptedData = CredentialsService.redact(
|
||||||
await CredentialsService.decrypt(key, credential),
|
CredentialsService.decrypt(credential),
|
||||||
credential,
|
credential,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -106,16 +105,15 @@ credentialsController.post(
|
||||||
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
|
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
|
||||||
const { credentials } = req.body;
|
const { credentials } = req.body;
|
||||||
|
|
||||||
const encryptionKey = await CredentialsService.getEncryptionKey();
|
|
||||||
const sharing = await CredentialsService.getSharing(req.user, credentials.id);
|
const sharing = await CredentialsService.getSharing(req.user, credentials.id);
|
||||||
|
|
||||||
const mergedCredentials = deepCopy(credentials);
|
const mergedCredentials = deepCopy(credentials);
|
||||||
if (mergedCredentials.data && sharing?.credentials) {
|
if (mergedCredentials.data && sharing?.credentials) {
|
||||||
const decryptedData = await CredentialsService.decrypt(encryptionKey, sharing.credentials);
|
const decryptedData = CredentialsService.decrypt(sharing.credentials);
|
||||||
mergedCredentials.data = CredentialsService.unredact(mergedCredentials.data, decryptedData);
|
mergedCredentials.data = CredentialsService.unredact(mergedCredentials.data, decryptedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CredentialsService.test(req.user, encryptionKey, mergedCredentials);
|
return CredentialsService.test(req.user, mergedCredentials);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -127,8 +125,7 @@ credentialsController.post(
|
||||||
ResponseHelper.send(async (req: CredentialRequest.Create) => {
|
ResponseHelper.send(async (req: CredentialRequest.Create) => {
|
||||||
const newCredential = await CredentialsService.prepareCreateData(req.body);
|
const newCredential = await CredentialsService.prepareCreateData(req.body);
|
||||||
|
|
||||||
const key = await CredentialsService.getEncryptionKey();
|
const encryptedData = CredentialsService.createEncryptedData(null, newCredential);
|
||||||
const encryptedData = CredentialsService.createEncryptedData(key, null, newCredential);
|
|
||||||
const credential = await CredentialsService.save(newCredential, encryptedData, req.user);
|
const credential = await CredentialsService.save(newCredential, encryptedData, req.user);
|
||||||
|
|
||||||
void Container.get(InternalHooks).onUserCreatedCredentials({
|
void Container.get(InternalHooks).onUserCreatedCredentials({
|
||||||
|
@ -165,14 +162,12 @@ credentialsController.patch(
|
||||||
|
|
||||||
const { credentials: credential } = sharing;
|
const { credentials: credential } = sharing;
|
||||||
|
|
||||||
const key = await CredentialsService.getEncryptionKey();
|
const decryptedData = CredentialsService.decrypt(credential);
|
||||||
const decryptedData = await CredentialsService.decrypt(key, credential);
|
|
||||||
const preparedCredentialData = await CredentialsService.prepareUpdateData(
|
const preparedCredentialData = await CredentialsService.prepareUpdateData(
|
||||||
req.body,
|
req.body,
|
||||||
decryptedData,
|
decryptedData,
|
||||||
);
|
);
|
||||||
const newCredentialData = CredentialsService.createEncryptedData(
|
const newCredentialData = CredentialsService.createEncryptedData(
|
||||||
key,
|
|
||||||
credentialId,
|
credentialId,
|
||||||
preparedCredentialData,
|
preparedCredentialData,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Credentials, UserSettings } from 'n8n-core';
|
import { Credentials } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
ICredentialsDecrypted,
|
ICredentialsDecrypted,
|
||||||
|
@ -12,10 +12,9 @@ import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
|
||||||
import { In, Like } from 'typeorm';
|
import { In, Like } from 'typeorm';
|
||||||
|
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
|
||||||
import type { ICredentialsDb } from '@/Interfaces';
|
import type { ICredentialsDb } from '@/Interfaces';
|
||||||
import { CredentialsHelper, createCredentialsFromCredentialsEntity } from '@/CredentialsHelper';
|
import { CredentialsHelper, createCredentialsFromCredentialsEntity } from '@/CredentialsHelper';
|
||||||
import { CREDENTIAL_BLANKING_VALUE, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
|
||||||
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||||
import { validateEntity } from '@/GenericHelpers';
|
import { validateEntity } from '@/GenericHelpers';
|
||||||
|
@ -205,18 +204,14 @@ export class CredentialsService {
|
||||||
return updateData;
|
return updateData;
|
||||||
}
|
}
|
||||||
|
|
||||||
static createEncryptedData(
|
static createEncryptedData(credentialId: string | null, data: CredentialsEntity): ICredentialsDb {
|
||||||
encryptionKey: string,
|
|
||||||
credentialId: string | null,
|
|
||||||
data: CredentialsEntity,
|
|
||||||
): ICredentialsDb {
|
|
||||||
const credentials = new Credentials(
|
const credentials = new Credentials(
|
||||||
{ id: credentialId, name: data.name },
|
{ id: credentialId, name: data.name },
|
||||||
data.type,
|
data.type,
|
||||||
data.nodesAccess,
|
data.nodesAccess,
|
||||||
);
|
);
|
||||||
|
|
||||||
credentials.setData(data.data as unknown as ICredentialDataDecryptedObject, encryptionKey);
|
credentials.setData(data.data as unknown as ICredentialDataDecryptedObject);
|
||||||
|
|
||||||
const newCredentialData = credentials.getDataToSave() as ICredentialsDb;
|
const newCredentialData = credentials.getDataToSave() as ICredentialsDb;
|
||||||
|
|
||||||
|
@ -226,22 +221,9 @@ export class CredentialsService {
|
||||||
return newCredentialData;
|
return newCredentialData;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getEncryptionKey(): Promise<string> {
|
static decrypt(credential: CredentialsEntity): ICredentialDataDecryptedObject {
|
||||||
try {
|
|
||||||
return await UserSettings.getEncryptionKey();
|
|
||||||
} catch (error) {
|
|
||||||
throw new ResponseHelper.InternalServerError(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async decrypt(
|
|
||||||
encryptionKey: string,
|
|
||||||
credential: CredentialsEntity,
|
|
||||||
): Promise<ICredentialDataDecryptedObject> {
|
|
||||||
const coreCredential = createCredentialsFromCredentialsEntity(credential);
|
const coreCredential = createCredentialsFromCredentialsEntity(credential);
|
||||||
const data = coreCredential.getData(encryptionKey);
|
return coreCredential.getData();
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async update(
|
static async update(
|
||||||
|
@ -303,11 +285,9 @@ export class CredentialsService {
|
||||||
|
|
||||||
static async test(
|
static async test(
|
||||||
user: User,
|
user: User,
|
||||||
encryptionKey: string,
|
|
||||||
credentials: ICredentialsDecrypted,
|
credentials: ICredentialsDecrypted,
|
||||||
): Promise<INodeCredentialTestResult> {
|
): Promise<INodeCredentialTestResult> {
|
||||||
const helper = new CredentialsHelper(encryptionKey);
|
const helper = Container.get(CredentialsHelper);
|
||||||
|
|
||||||
return helper.testCredentials(user, credentials.type, credentials);
|
return helper.testCredentials(user, credentials.type, credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,8 @@ import omit from 'lodash/omit';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import split from 'lodash/split';
|
import split from 'lodash/split';
|
||||||
import unset from 'lodash/unset';
|
import unset from 'lodash/unset';
|
||||||
import { Credentials, UserSettings } from 'n8n-core';
|
import { Credentials } from 'n8n-core';
|
||||||
import type {
|
import type { WorkflowExecuteMode, INodeCredentialsDetails } from 'n8n-workflow';
|
||||||
WorkflowExecuteMode,
|
|
||||||
INodeCredentialsDetails,
|
|
||||||
ICredentialsEncrypted,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { LoggerProxy, jsonStringify } from 'n8n-workflow';
|
import { LoggerProxy, jsonStringify } from 'n8n-workflow';
|
||||||
import { resolve as pathResolve } from 'path';
|
import { resolve as pathResolve } from 'path';
|
||||||
|
|
||||||
|
@ -76,20 +72,13 @@ oauth2CredentialController.get(
|
||||||
throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL);
|
throw new ResponseHelper.NotFoundError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
let encryptionKey: string;
|
|
||||||
try {
|
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
} catch (error) {
|
|
||||||
throw new ResponseHelper.InternalServerError((error as Error).message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
||||||
|
|
||||||
const credentialType = (credential as unknown as ICredentialsEncrypted).type;
|
const credentialType = credential.type;
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const timezone = config.getEnv('generic.timezone');
|
const timezone = config.getEnv('generic.timezone');
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = Container.get(CredentialsHelper);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
additionalData,
|
additionalData,
|
||||||
credential as INodeCredentialsDetails,
|
credential as INodeCredentialsDetails,
|
||||||
|
@ -152,7 +141,7 @@ oauth2CredentialController.get(
|
||||||
const credentials = new Credentials(
|
const credentials = new Credentials(
|
||||||
credential as INodeCredentialsDetails,
|
credential as INodeCredentialsDetails,
|
||||||
credentialType,
|
credentialType,
|
||||||
(credential as unknown as ICredentialsEncrypted).nodesAccess,
|
credential.nodesAccess,
|
||||||
);
|
);
|
||||||
decryptedDataOriginal.csrfSecret = csrfSecret;
|
decryptedDataOriginal.csrfSecret = csrfSecret;
|
||||||
|
|
||||||
|
@ -166,7 +155,7 @@ oauth2CredentialController.get(
|
||||||
decryptedDataOriginal.codeVerifier = code_verifier;
|
decryptedDataOriginal.codeVerifier = code_verifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
|
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
|
@ -228,16 +217,15 @@ oauth2CredentialController.get(
|
||||||
return renderCallbackError(res, errorMessage);
|
return renderCallbackError(res, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(state.cid);
|
const additionalData = await WorkflowExecuteAdditionalData.getBase(state.cid);
|
||||||
|
|
||||||
const mode: WorkflowExecuteMode = 'internal';
|
const mode: WorkflowExecuteMode = 'internal';
|
||||||
const timezone = config.getEnv('generic.timezone');
|
const timezone = config.getEnv('generic.timezone');
|
||||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
const credentialsHelper = Container.get(CredentialsHelper);
|
||||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||||
additionalData,
|
additionalData,
|
||||||
credential as INodeCredentialsDetails,
|
credential as INodeCredentialsDetails,
|
||||||
(credential as unknown as ICredentialsEncrypted).type,
|
credential.type,
|
||||||
mode,
|
mode,
|
||||||
timezone,
|
timezone,
|
||||||
true,
|
true,
|
||||||
|
@ -245,7 +233,7 @@ oauth2CredentialController.get(
|
||||||
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
|
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(
|
||||||
additionalData,
|
additionalData,
|
||||||
decryptedDataOriginal,
|
decryptedDataOriginal,
|
||||||
(credential as unknown as ICredentialsEncrypted).type,
|
credential.type,
|
||||||
mode,
|
mode,
|
||||||
timezone,
|
timezone,
|
||||||
);
|
);
|
||||||
|
@ -330,10 +318,10 @@ oauth2CredentialController.get(
|
||||||
|
|
||||||
const credentials = new Credentials(
|
const credentials = new Credentials(
|
||||||
credential as INodeCredentialsDetails,
|
credential as INodeCredentialsDetails,
|
||||||
(credential as unknown as ICredentialsEncrypted).type,
|
credential.type,
|
||||||
(credential as unknown as ICredentialsEncrypted).nodesAccess,
|
credential.nodesAccess,
|
||||||
);
|
);
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
newCredentialsData.updatedAt = new Date();
|
newCredentialsData.updatedAt = new Date();
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { Container } from 'typedi';
|
||||||
import type { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
|
import type { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
|
||||||
import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
|
import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';
|
||||||
import type { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions';
|
import type { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
import { entities } from './entities';
|
import { entities } from './entities';
|
||||||
import { mysqlMigrations } from './migrations/mysqldb';
|
import { mysqlMigrations } from './migrations/mysqldb';
|
||||||
|
@ -21,7 +22,7 @@ const getDBConnectionOptions = (dbType: DatabaseType) => {
|
||||||
configDBType === 'sqlite'
|
configDBType === 'sqlite'
|
||||||
? {
|
? {
|
||||||
database: path.resolve(
|
database: path.resolve(
|
||||||
UserSettings.getUserN8nFolderPath(),
|
Container.get(InstanceSettings).n8nFolder,
|
||||||
config.getEnv('database.sqlite.database'),
|
config.getEnv('database.sqlite.database'),
|
||||||
),
|
),
|
||||||
enableWAL: config.getEnv('database.sqlite.enableWAL'),
|
enableWAL: config.getEnv('database.sqlite.enableWAL'),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { statSync } from 'fs';
|
import { statSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Container } from 'typedi';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import type { MigrationContext, IrreversibleMigration } from '@db/types';
|
import type { MigrationContext, IrreversibleMigration } from '@db/types';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
|
@ -191,7 +192,7 @@ const migrationsPruningEnabled = process.env.MIGRATIONS_PRUNING_ENABLED === 'tru
|
||||||
|
|
||||||
function getSqliteDbFileSize(): number {
|
function getSqliteDbFileSize(): number {
|
||||||
const filename = path.resolve(
|
const filename = path.resolve(
|
||||||
UserSettings.getUserN8nFolderPath(),
|
Container.get(InstanceSettings).n8nFolder,
|
||||||
config.getEnv('database.sqlite.database'),
|
config.getEnv('database.sqlite.database'),
|
||||||
);
|
);
|
||||||
const { size } = statSync(filename);
|
const { size } = statSync(filename);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { readFileSync, rmSync } from 'fs';
|
import { readFileSync, rmSync } from 'fs';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import type { ObjectLiteral } from 'typeorm';
|
import type { ObjectLiteral } from 'typeorm';
|
||||||
import type { QueryRunner } from 'typeorm/query-runner/QueryRunner';
|
import type { QueryRunner } from 'typeorm/query-runner/QueryRunner';
|
||||||
import { jsonParse } from 'n8n-workflow';
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
@ -16,9 +16,10 @@ const logger = getLogger();
|
||||||
const PERSONALIZATION_SURVEY_FILENAME = 'personalizationSurvey.json';
|
const PERSONALIZATION_SURVEY_FILENAME = 'personalizationSurvey.json';
|
||||||
|
|
||||||
function loadSurveyFromDisk(): string | null {
|
function loadSurveyFromDisk(): string | null {
|
||||||
const userSettingsPath = UserSettings.getUserN8nFolderPath();
|
|
||||||
try {
|
try {
|
||||||
const filename = `${userSettingsPath}/${PERSONALIZATION_SURVEY_FILENAME}`;
|
const filename = `${
|
||||||
|
Container.get(InstanceSettings).n8nFolder
|
||||||
|
}/${PERSONALIZATION_SURVEY_FILENAME}`;
|
||||||
const surveyFile = readFileSync(filename, 'utf-8');
|
const surveyFile = readFileSync(filename, 'utf-8');
|
||||||
rmSync(filename);
|
rmSync(filename);
|
||||||
const personalizationSurvey = JSON.parse(surveyFile) as object;
|
const personalizationSurvey = JSON.parse(surveyFile) as object;
|
||||||
|
|
|
@ -12,14 +12,10 @@ import type { SourceControlPreferences } from './types/sourceControlPreferences'
|
||||||
import {
|
import {
|
||||||
SOURCE_CONTROL_DEFAULT_EMAIL,
|
SOURCE_CONTROL_DEFAULT_EMAIL,
|
||||||
SOURCE_CONTROL_DEFAULT_NAME,
|
SOURCE_CONTROL_DEFAULT_NAME,
|
||||||
SOURCE_CONTROL_GIT_FOLDER,
|
|
||||||
SOURCE_CONTROL_README,
|
SOURCE_CONTROL_README,
|
||||||
SOURCE_CONTROL_SSH_FOLDER,
|
|
||||||
SOURCE_CONTROL_SSH_KEY_NAME,
|
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
import { SourceControlGitService } from './sourceControlGit.service.ee';
|
import { SourceControlGitService } from './sourceControlGit.service.ee';
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import type { PushResult } from 'simple-git';
|
import type { PushResult } from 'simple-git';
|
||||||
import { SourceControlExportService } from './sourceControlExport.service.ee';
|
import { SourceControlExportService } from './sourceControlExport.service.ee';
|
||||||
import { BadRequestError } from '@/ResponseHelper';
|
import { BadRequestError } from '@/ResponseHelper';
|
||||||
|
@ -55,10 +51,10 @@ export class SourceControlService {
|
||||||
private sourceControlImportService: SourceControlImportService,
|
private sourceControlImportService: SourceControlImportService,
|
||||||
private tagRepository: TagRepository,
|
private tagRepository: TagRepository,
|
||||||
) {
|
) {
|
||||||
const userFolder = UserSettings.getUserN8nFolderPath();
|
const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService;
|
||||||
this.sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER);
|
this.gitFolder = gitFolder;
|
||||||
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
|
this.sshFolder = sshFolder;
|
||||||
this.sshKeyName = path.join(this.sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
|
this.sshKeyName = sshKeyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises';
|
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises';
|
||||||
import { rmSync } from 'fs';
|
import { rmSync } from 'fs';
|
||||||
import { Credentials, UserSettings } from 'n8n-core';
|
import { Credentials, InstanceSettings } from 'n8n-core';
|
||||||
import type { ExportableWorkflow } from './types/exportableWorkflow';
|
import type { ExportableWorkflow } from './types/exportableWorkflow';
|
||||||
import type { ExportableCredential } from './types/exportableCredential';
|
import type { ExportableCredential } from './types/exportableCredential';
|
||||||
import type { ExportResult } from './types/exportResult';
|
import type { ExportResult } from './types/exportResult';
|
||||||
|
@ -39,9 +39,9 @@ export class SourceControlExportService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly variablesService: VariablesService,
|
private readonly variablesService: VariablesService,
|
||||||
private readonly tagRepository: TagRepository,
|
private readonly tagRepository: TagRepository,
|
||||||
|
instanceSettings: InstanceSettings,
|
||||||
) {
|
) {
|
||||||
const userFolder = UserSettings.getUserN8nFolderPath();
|
this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
|
||||||
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
|
|
||||||
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
|
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
|
||||||
this.credentialExportFolder = path.join(
|
this.credentialExportFolder = path.join(
|
||||||
this.gitFolder,
|
this.gitFolder,
|
||||||
|
@ -248,12 +248,11 @@ export class SourceControlExportService {
|
||||||
(remote) => foundCredentialIds.findIndex((local) => local === remote) === -1,
|
(remote) => foundCredentialIds.findIndex((local) => local === remote) === -1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
credentialsToBeExported.map(async (sharedCredential) => {
|
credentialsToBeExported.map(async (sharedCredential) => {
|
||||||
const { name, type, nodesAccess, data, id } = sharedCredential.credentials;
|
const { name, type, nodesAccess, data, id } = sharedCredential.credentials;
|
||||||
const credentialObject = new Credentials({ id, name }, type, nodesAccess, data);
|
const credentialObject = new Credentials({ id, name }, type, nodesAccess, data);
|
||||||
const plainData = credentialObject.getData(encryptionKey);
|
const plainData = credentialObject.getData();
|
||||||
const sanitizedData = this.replaceCredentialData(plainData);
|
const sanitizedData = this.replaceCredentialData(plainData);
|
||||||
const fileName = this.getCredentialsPath(sharedCredential.credentials.id);
|
const fileName = this.getCredentialsPath(sharedCredential.credentials.id);
|
||||||
const sanitizedCredential: ExportableCredential = {
|
const sanitizedCredential: ExportableCredential = {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import * as Db from '@/Db';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
import { LoggerProxy, jsonParse } from 'n8n-workflow';
|
import { LoggerProxy, jsonParse } from 'n8n-workflow';
|
||||||
import { readFile as fsReadFile } from 'fs/promises';
|
import { readFile as fsReadFile } from 'fs/promises';
|
||||||
import { Credentials, UserSettings } from 'n8n-core';
|
import { Credentials, InstanceSettings } from 'n8n-core';
|
||||||
import type { IWorkflowToImport } from '@/Interfaces';
|
import type { IWorkflowToImport } from '@/Interfaces';
|
||||||
import type { ExportableCredential } from './types/exportableCredential';
|
import type { ExportableCredential } from './types/exportableCredential';
|
||||||
import type { Variables } from '@db/entities/Variables';
|
import type { Variables } from '@db/entities/Variables';
|
||||||
|
@ -41,9 +41,9 @@ export class SourceControlImportService {
|
||||||
private readonly variablesService: VariablesService,
|
private readonly variablesService: VariablesService,
|
||||||
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
|
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
|
||||||
private readonly tagRepository: TagRepository,
|
private readonly tagRepository: TagRepository,
|
||||||
|
instanceSettings: InstanceSettings,
|
||||||
) {
|
) {
|
||||||
const userFolder = UserSettings.getUserN8nFolderPath();
|
this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
|
||||||
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
|
|
||||||
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
|
this.workflowExportFolder = path.join(this.gitFolder, SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER);
|
||||||
this.credentialExportFolder = path.join(
|
this.credentialExportFolder = path.join(
|
||||||
this.gitFolder,
|
this.gitFolder,
|
||||||
|
@ -81,69 +81,6 @@ export class SourceControlImportService {
|
||||||
return workflowOwnerRole;
|
return workflowOwnerRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async importCredentialsFromFiles(
|
|
||||||
userId: string,
|
|
||||||
): Promise<Array<{ id: string; name: string; type: string }>> {
|
|
||||||
const credentialFiles = await glob('*.json', {
|
|
||||||
cwd: this.credentialExportFolder,
|
|
||||||
absolute: true,
|
|
||||||
});
|
|
||||||
const existingCredentials = await Db.collections.Credentials.find();
|
|
||||||
const ownerCredentialRole = await this.getCredentialOwnerRole();
|
|
||||||
const ownerGlobalRole = await this.getOwnerGlobalRole();
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
let importCredentialsResult: Array<{ id: string; name: string; type: string }> = [];
|
|
||||||
importCredentialsResult = await Promise.all(
|
|
||||||
credentialFiles.map(async (file) => {
|
|
||||||
LoggerProxy.debug(`Importing credentials file ${file}`);
|
|
||||||
const credential = jsonParse<ExportableCredential>(
|
|
||||||
await fsReadFile(file, { encoding: 'utf8' }),
|
|
||||||
);
|
|
||||||
const existingCredential = existingCredentials.find(
|
|
||||||
(e) => e.id === credential.id && e.type === credential.type,
|
|
||||||
);
|
|
||||||
const sharedOwner = await Db.collections.SharedCredentials.findOne({
|
|
||||||
select: ['userId'],
|
|
||||||
where: {
|
|
||||||
credentialsId: credential.id,
|
|
||||||
roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { name, type, data, id, nodesAccess } = credential;
|
|
||||||
const newCredentialObject = new Credentials({ id, name }, type, []);
|
|
||||||
if (existingCredential?.data) {
|
|
||||||
newCredentialObject.data = existingCredential.data;
|
|
||||||
} else {
|
|
||||||
newCredentialObject.setData(data, encryptionKey);
|
|
||||||
}
|
|
||||||
newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || [];
|
|
||||||
|
|
||||||
LoggerProxy.debug(`Updating credential id ${newCredentialObject.id as string}`);
|
|
||||||
await Db.collections.Credentials.upsert(newCredentialObject, ['id']);
|
|
||||||
|
|
||||||
if (!sharedOwner) {
|
|
||||||
const newSharedCredential = new SharedCredentials();
|
|
||||||
newSharedCredential.credentialsId = newCredentialObject.id as string;
|
|
||||||
newSharedCredential.userId = userId;
|
|
||||||
newSharedCredential.roleId = ownerGlobalRole.id;
|
|
||||||
|
|
||||||
await Db.collections.SharedCredentials.upsert({ ...newSharedCredential }, [
|
|
||||||
'credentialsId',
|
|
||||||
'userId',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: newCredentialObject.id as string,
|
|
||||||
name: newCredentialObject.name,
|
|
||||||
type: newCredentialObject.type,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return importCredentialsResult.filter((e) => e !== undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getRemoteVersionIdsFromFiles(): Promise<SourceControlWorkflowVersionId[]> {
|
public async getRemoteVersionIdsFromFiles(): Promise<SourceControlWorkflowVersionId[]> {
|
||||||
const remoteWorkflowFiles = await glob('*.json', {
|
const remoteWorkflowFiles = await glob('*.json', {
|
||||||
cwd: this.workflowExportFolder,
|
cwd: this.workflowExportFolder,
|
||||||
|
@ -407,7 +344,6 @@ export class SourceControlImportService {
|
||||||
roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]),
|
roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
let importCredentialsResult: Array<{ id: string; name: string; type: string }> = [];
|
let importCredentialsResult: Array<{ id: string; name: string; type: string }> = [];
|
||||||
importCredentialsResult = await Promise.all(
|
importCredentialsResult = await Promise.all(
|
||||||
candidates.map(async (candidate) => {
|
candidates.map(async (candidate) => {
|
||||||
|
@ -427,7 +363,7 @@ export class SourceControlImportService {
|
||||||
if (existingCredential?.data) {
|
if (existingCredential?.data) {
|
||||||
newCredentialObject.data = existingCredential.data;
|
newCredentialObject.data = existingCredential.data;
|
||||||
} else {
|
} else {
|
||||||
newCredentialObject.setData(data, encryptionKey);
|
newCredentialObject.setData(data);
|
||||||
}
|
}
|
||||||
newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || [];
|
newCredentialObject.nodesAccess = nodesAccess || existingCredential?.nodesAccess || [];
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
isSourceControlLicensed,
|
isSourceControlLicensed,
|
||||||
sourceControlFoldersExistCheck,
|
sourceControlFoldersExistCheck,
|
||||||
} from './sourceControlHelper.ee';
|
} from './sourceControlHelper.ee';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import { LoggerProxy, jsonParse } from 'n8n-workflow';
|
import { LoggerProxy, jsonParse } from 'n8n-workflow';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import {
|
import {
|
||||||
|
@ -26,16 +26,15 @@ import config from '@/config';
|
||||||
export class SourceControlPreferencesService {
|
export class SourceControlPreferencesService {
|
||||||
private _sourceControlPreferences: SourceControlPreferences = new SourceControlPreferences();
|
private _sourceControlPreferences: SourceControlPreferences = new SourceControlPreferences();
|
||||||
|
|
||||||
private sshKeyName: string;
|
readonly sshKeyName: string;
|
||||||
|
|
||||||
private sshFolder: string;
|
readonly sshFolder: string;
|
||||||
|
|
||||||
private gitFolder: string;
|
readonly gitFolder: string;
|
||||||
|
|
||||||
constructor() {
|
constructor(instanceSettings: InstanceSettings) {
|
||||||
const userFolder = UserSettings.getUserN8nFolderPath();
|
this.sshFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_SSH_FOLDER);
|
||||||
this.sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER);
|
this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
|
||||||
this.gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
|
|
||||||
this.sshKeyName = path.join(this.sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
|
this.sshKeyName = path.join(this.sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ import type {
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { CredentialsHelper } from '@/CredentialsHelper';
|
import { CredentialsHelper } from '@/CredentialsHelper';
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import { Agent as HTTPSAgent } from 'https';
|
import { Agent as HTTPSAgent } from 'https';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { isLogStreamingEnabled } from '../MessageEventBus/MessageEventBusHelper';
|
import { isLogStreamingEnabled } from '../MessageEventBus/MessageEventBusHelper';
|
||||||
|
@ -26,6 +25,7 @@ import { eventMessageGenericDestinationTestEvent } from '../EventMessageClasses/
|
||||||
import { MessageEventBus } from '../MessageEventBus/MessageEventBus';
|
import { MessageEventBus } from '../MessageEventBus/MessageEventBus';
|
||||||
import type { MessageWithCallback } from '../MessageEventBus/MessageEventBus';
|
import type { MessageWithCallback } from '../MessageEventBus/MessageEventBus';
|
||||||
import * as SecretsHelpers from '@/ExternalSecrets/externalSecretsHelper.ee';
|
import * as SecretsHelpers from '@/ExternalSecrets/externalSecretsHelper.ee';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
export const isMessageEventBusDestinationWebhookOptions = (
|
export const isMessageEventBusDestinationWebhookOptions = (
|
||||||
candidate: unknown,
|
candidate: unknown,
|
||||||
|
@ -135,13 +135,7 @@ export class MessageEventBusDestinationWebhook
|
||||||
} as AxiosRequestConfig;
|
} as AxiosRequestConfig;
|
||||||
|
|
||||||
if (this.credentialsHelper === undefined) {
|
if (this.credentialsHelper === undefined) {
|
||||||
let encryptionKey: string | undefined;
|
this.credentialsHelper = Container.get(CredentialsHelper);
|
||||||
try {
|
|
||||||
encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
} catch {}
|
|
||||||
if (encryptionKey) {
|
|
||||||
this.credentialsHelper = new CredentialsHelper(encryptionKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendQuery = this.sendQuery;
|
const sendQuery = this.sendQuery;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
|
||||||
import { isEventMessageOptions } from '../EventMessageClasses/AbstractEventMessage';
|
import { isEventMessageOptions } from '../EventMessageClasses/AbstractEventMessage';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import path, { parse } from 'path';
|
import path, { parse } from 'path';
|
||||||
import { Worker } from 'worker_threads';
|
import { Worker } from 'worker_threads';
|
||||||
import { createReadStream, existsSync, rmSync } from 'fs';
|
import { createReadStream, existsSync, rmSync } from 'fs';
|
||||||
|
@ -19,6 +19,7 @@ import {
|
||||||
} from '../EventMessageClasses/EventMessageConfirm';
|
} from '../EventMessageClasses/EventMessageConfirm';
|
||||||
import { once as eventOnce } from 'events';
|
import { once as eventOnce } from 'events';
|
||||||
import { inTest } from '@/constants';
|
import { inTest } from '@/constants';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
interface MessageEventBusLogWriterConstructorOptions {
|
interface MessageEventBusLogWriterConstructorOptions {
|
||||||
logBaseName?: string;
|
logBaseName?: string;
|
||||||
|
@ -66,7 +67,7 @@ export class MessageEventBusLogWriter {
|
||||||
MessageEventBusLogWriter.instance = new MessageEventBusLogWriter();
|
MessageEventBusLogWriter.instance = new MessageEventBusLogWriter();
|
||||||
MessageEventBusLogWriter.options = {
|
MessageEventBusLogWriter.options = {
|
||||||
logFullBasePath: path.join(
|
logFullBasePath: path.join(
|
||||||
options?.logBasePath ?? UserSettings.getUserN8nFolderPath(),
|
options?.logBasePath ?? Container.get(InstanceSettings).n8nFolder,
|
||||||
options?.logBaseName ?? config.getEnv('eventBus.logWriter.logBaseName'),
|
options?.logBaseName ?? config.getEnv('eventBus.logWriter.logBaseName'),
|
||||||
),
|
),
|
||||||
keepNumberOfFiles:
|
keepNumberOfFiles:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import type { PostHog } from 'posthog-node';
|
import type { PostHog } from 'posthog-node';
|
||||||
import type { FeatureFlags, ITelemetryTrackProperties } from 'n8n-workflow';
|
import type { FeatureFlags, ITelemetryTrackProperties } from 'n8n-workflow';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import type { PublicUser } from '@/Interfaces';
|
import type { PublicUser } from '@/Interfaces';
|
||||||
|
|
||||||
|
@ -8,10 +9,9 @@ import type { PublicUser } from '@/Interfaces';
|
||||||
export class PostHogClient {
|
export class PostHogClient {
|
||||||
private postHog?: PostHog;
|
private postHog?: PostHog;
|
||||||
|
|
||||||
private instanceId?: string;
|
constructor(private readonly instanceSettings: InstanceSettings) {}
|
||||||
|
|
||||||
async init(instanceId: string) {
|
async init() {
|
||||||
this.instanceId = instanceId;
|
|
||||||
const enabled = config.getEnv('diagnostics.enabled');
|
const enabled = config.getEnv('diagnostics.enabled');
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return;
|
||||||
|
@ -46,7 +46,7 @@ export class PostHogClient {
|
||||||
async getFeatureFlags(user: Pick<PublicUser, 'id' | 'createdAt'>): Promise<FeatureFlags> {
|
async getFeatureFlags(user: Pick<PublicUser, 'id' | 'createdAt'>): Promise<FeatureFlags> {
|
||||||
if (!this.postHog) return {};
|
if (!this.postHog) return {};
|
||||||
|
|
||||||
const fullId = [this.instanceId, user.id].join('#');
|
const fullId = [this.instanceSettings.instanceId, user.id].join('#');
|
||||||
|
|
||||||
// cannot use local evaluation because that requires PostHog personal api key with org-wide
|
// cannot use local evaluation because that requires PostHog personal api key with org-wide
|
||||||
// https://github.com/PostHog/posthog/issues/4849
|
// https://github.com/PostHog/posthog/issues/4849
|
||||||
|
|
|
@ -6,7 +6,7 @@ import axios from 'axios';
|
||||||
|
|
||||||
import { LoggerProxy as Logger } from 'n8n-workflow';
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import type { PackageDirectoryLoader } from 'n8n-core';
|
import type { PackageDirectoryLoader } from 'n8n-core';
|
||||||
|
|
||||||
import { toError } from '@/utils';
|
import { toError } from '@/utils';
|
||||||
|
@ -47,6 +47,7 @@ export class CommunityPackagesService {
|
||||||
missingPackages: string[] = [];
|
missingPackages: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly instanceSettings: InstanceSettings,
|
||||||
private readonly installedPackageRepository: InstalledPackagesRepository,
|
private readonly installedPackageRepository: InstalledPackagesRepository,
|
||||||
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
) {}
|
) {}
|
||||||
|
@ -114,7 +115,7 @@ export class CommunityPackagesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeNpmCommand(command: string, options?: { doNotHandleError?: boolean }) {
|
async executeNpmCommand(command: string, options?: { doNotHandleError?: boolean }) {
|
||||||
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
const downloadFolder = this.instanceSettings.nodesDownloadDir;
|
||||||
|
|
||||||
const execOptions = {
|
const execOptions = {
|
||||||
cwd: downloadFolder,
|
cwd: downloadFolder,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
INodeTypeBaseDescription,
|
INodeTypeBaseDescription,
|
||||||
ITelemetrySettings,
|
ITelemetrySettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
import { GENERATED_STATIC_DIR, LICENSE_FEATURES } from '@/constants';
|
import { GENERATED_STATIC_DIR, LICENSE_FEATURES } from '@/constants';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
|
@ -40,6 +41,7 @@ export class FrontendService {
|
||||||
private readonly credentialsOverwrites: CredentialsOverwrites,
|
private readonly credentialsOverwrites: CredentialsOverwrites,
|
||||||
private readonly license: License,
|
private readonly license: License,
|
||||||
private readonly mailer: UserManagementMailer,
|
private readonly mailer: UserManagementMailer,
|
||||||
|
private readonly instanceSettings: InstanceSettings,
|
||||||
) {
|
) {
|
||||||
this.initSettings();
|
this.initSettings();
|
||||||
}
|
}
|
||||||
|
@ -87,7 +89,7 @@ export class FrontendService {
|
||||||
endpoint: config.getEnv('versionNotifications.endpoint'),
|
endpoint: config.getEnv('versionNotifications.endpoint'),
|
||||||
infoUrl: config.getEnv('versionNotifications.infoUrl'),
|
infoUrl: config.getEnv('versionNotifications.infoUrl'),
|
||||||
},
|
},
|
||||||
instanceId: '',
|
instanceId: this.instanceSettings.instanceId,
|
||||||
telemetry: telemetrySettings,
|
telemetry: telemetrySettings,
|
||||||
posthog: {
|
posthog: {
|
||||||
enabled: config.getEnv('diagnostics.enabled'),
|
enabled: config.getEnv('diagnostics.enabled'),
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type { RedisServicePubSubPublisher } from '../../redis/RedisServicePubSub
|
||||||
|
|
||||||
export interface WorkerCommandReceivedHandlerOptions {
|
export interface WorkerCommandReceivedHandlerOptions {
|
||||||
queueModeId: string;
|
queueModeId: string;
|
||||||
instanceId: string;
|
|
||||||
redisPublisher: RedisServicePubSubPublisher;
|
redisPublisher: RedisServicePubSubPublisher;
|
||||||
getRunningJobIds: () => string[];
|
getRunningJobIds: () => string[];
|
||||||
getRunningJobsSummary: () => WorkerJobStatusSummary[];
|
getRunningJobsSummary: () => WorkerJobStatusSummary[];
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { LicenseService } from '@/license/License.service';
|
||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
import Container, { Service } from 'typedi';
|
import Container, { Service } from 'typedi';
|
||||||
import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee';
|
import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
|
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
|
||||||
|
|
||||||
|
@ -30,8 +31,6 @@ interface IExecutionsBuffer {
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class Telemetry {
|
export class Telemetry {
|
||||||
private instanceId: string;
|
|
||||||
|
|
||||||
private rudderStack?: RudderStack;
|
private rudderStack?: RudderStack;
|
||||||
|
|
||||||
private pulseIntervalReference: NodeJS.Timeout;
|
private pulseIntervalReference: NodeJS.Timeout;
|
||||||
|
@ -41,12 +40,9 @@ export class Telemetry {
|
||||||
constructor(
|
constructor(
|
||||||
private postHog: PostHogClient,
|
private postHog: PostHogClient,
|
||||||
private license: License,
|
private license: License,
|
||||||
|
private readonly instanceSettings: InstanceSettings,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
setInstanceId(instanceId: string) {
|
|
||||||
this.instanceId = instanceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const enabled = config.getEnv('diagnostics.enabled');
|
const enabled = config.getEnv('diagnostics.enabled');
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
@ -172,15 +168,13 @@ export class Telemetry {
|
||||||
async identify(traits?: {
|
async identify(traits?: {
|
||||||
[key: string]: string | number | boolean | object | undefined | null;
|
[key: string]: string | number | boolean | object | undefined | null;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
const { instanceId } = this.instanceSettings;
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
if (this.rudderStack) {
|
if (this.rudderStack) {
|
||||||
this.rudderStack.identify(
|
this.rudderStack.identify(
|
||||||
{
|
{
|
||||||
userId: this.instanceId,
|
userId: instanceId,
|
||||||
traits: {
|
traits: { ...traits, instanceId },
|
||||||
...traits,
|
|
||||||
instanceId: this.instanceId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
resolve,
|
resolve,
|
||||||
);
|
);
|
||||||
|
@ -195,17 +189,18 @@ export class Telemetry {
|
||||||
properties: ITelemetryTrackProperties = {},
|
properties: ITelemetryTrackProperties = {},
|
||||||
{ withPostHog } = { withPostHog: false }, // whether to additionally track with PostHog
|
{ withPostHog } = { withPostHog: false }, // whether to additionally track with PostHog
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { instanceId } = this.instanceSettings;
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
if (this.rudderStack) {
|
if (this.rudderStack) {
|
||||||
const { user_id } = properties;
|
const { user_id } = properties;
|
||||||
const updatedProperties: ITelemetryTrackProperties = {
|
const updatedProperties: ITelemetryTrackProperties = {
|
||||||
...properties,
|
...properties,
|
||||||
instance_id: this.instanceId,
|
instance_id: instanceId,
|
||||||
version_cli: N8N_VERSION,
|
version_cli: N8N_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
userId: `${this.instanceId}${user_id ? `#${user_id}` : ''}`,
|
userId: `${instanceId}${user_id ? `#${user_id}` : ''}`,
|
||||||
event: eventName,
|
event: eventName,
|
||||||
properties: updatedProperties,
|
properties: updatedProperties,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { License } from '@/License';
|
||||||
import * as testDb from '../shared/testDb';
|
import * as testDb from '../shared/testDb';
|
||||||
import * as utils from '../shared/utils/';
|
import * as utils from '../shared/utils/';
|
||||||
import type { ExternalSecretsSettings, SecretsProviderState } from '@/Interfaces';
|
import type { ExternalSecretsSettings, SecretsProviderState } from '@/Interfaces';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Cipher } from 'n8n-core';
|
||||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import { AES, enc } from 'crypto-js';
|
|
||||||
import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee';
|
import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee';
|
||||||
import {
|
import {
|
||||||
DummyProvider,
|
DummyProvider,
|
||||||
|
@ -17,7 +16,7 @@ import {
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
|
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
|
||||||
import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
|
import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import { jsonParse, type IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
let authOwnerAgent: SuperAgentTest;
|
let authOwnerAgent: SuperAgentTest;
|
||||||
let authMemberAgent: SuperAgentTest;
|
let authMemberAgent: SuperAgentTest;
|
||||||
|
@ -28,29 +27,24 @@ const licenseLike = utils.mockInstance(License, {
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockProvidersInstance = new MockProviders();
|
const mockProvidersInstance = new MockProviders();
|
||||||
let providersMock: ExternalSecretsProviders = utils.mockInstance(
|
utils.mockInstance(ExternalSecretsProviders, mockProvidersInstance);
|
||||||
ExternalSecretsProviders,
|
|
||||||
mockProvidersInstance,
|
|
||||||
);
|
|
||||||
|
|
||||||
const testServer = utils.setupTestServer({ endpointGroups: ['externalSecrets'] });
|
const testServer = utils.setupTestServer({ endpointGroups: ['externalSecrets'] });
|
||||||
|
|
||||||
const connectedDate = '2023-08-01T12:32:29.000Z';
|
const connectedDate = '2023-08-01T12:32:29.000Z';
|
||||||
|
|
||||||
async function setExternalSecretsSettings(settings: ExternalSecretsSettings) {
|
async function setExternalSecretsSettings(settings: ExternalSecretsSettings) {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
return Container.get(SettingsRepository).saveEncryptedSecretsProviderSettings(
|
return Container.get(SettingsRepository).saveEncryptedSecretsProviderSettings(
|
||||||
AES.encrypt(JSON.stringify(settings), encryptionKey).toString(),
|
Container.get(Cipher).encrypt(settings),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | null> {
|
async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | null> {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
const encSettings = await Container.get(SettingsRepository).getEncryptedSecretsProviderSettings();
|
const encSettings = await Container.get(SettingsRepository).getEncryptedSecretsProviderSettings();
|
||||||
if (encSettings === null) {
|
if (encSettings === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return JSON.parse(AES.decrypt(encSettings, encryptionKey).toString(enc.Utf8));
|
return jsonParse(Container.get(Cipher).decrypt(encSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetManager = async () => {
|
const resetManager = async () => {
|
||||||
|
@ -61,6 +55,7 @@ const resetManager = async () => {
|
||||||
Container.get(SettingsRepository),
|
Container.get(SettingsRepository),
|
||||||
licenseLike,
|
licenseLike,
|
||||||
mockProvidersInstance,
|
mockProvidersInstance,
|
||||||
|
Container.get(Cipher),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -100,8 +95,6 @@ const getDummyProviderData = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.initEncryptionKey();
|
|
||||||
|
|
||||||
const owner = await testDb.createOwner();
|
const owner = await testDb.createOwner();
|
||||||
authOwnerAgent = testServer.authAgentFor(owner);
|
authOwnerAgent = testServer.authAgentFor(owner);
|
||||||
const member = await testDb.createUser();
|
const member = await testDb.createUser();
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import type { SuperAgentTest } from 'supertest';
|
import type { SuperAgentTest } from 'supertest';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import type { IUser } from 'n8n-workflow';
|
import type { IUser } from 'n8n-workflow';
|
||||||
|
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
|
||||||
import type { Credentials } from '@/requests';
|
import type { Credentials } from '@/requests';
|
||||||
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
|
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
|
@ -304,21 +302,6 @@ describe('GET /credentials/:id', () => {
|
||||||
expect(response.body.data).toBeUndefined(); // owner's cred not returned
|
expect(response.body.data).toBeUndefined(); // owner's cred not returned
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with missing encryption key', async () => {
|
|
||||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
|
||||||
|
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
|
||||||
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
|
||||||
|
|
||||||
const response = await authOwnerAgent
|
|
||||||
.get(`/credentials/${savedCredential.id}`)
|
|
||||||
.query({ includeData: true });
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(500);
|
|
||||||
|
|
||||||
mock.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return 404 if cred not found', async () => {
|
test('should return 404 if cred not found', async () => {
|
||||||
const response = await authOwnerAgent.get('/credentials/789');
|
const response = await authOwnerAgent.get('/credentials/789');
|
||||||
expect(response.statusCode).toBe(404);
|
expect(response.statusCode).toBe(404);
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import type { SuperAgentTest } from 'supertest';
|
import type { SuperAgentTest } from 'supertest';
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
|
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
|
||||||
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
|
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
|
||||||
import type { Credentials } from '@/requests';
|
import type { Credentials } from '@/requests';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
|
@ -130,17 +128,6 @@ describe('POST /credentials', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with missing encryption key', async () => {
|
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
|
||||||
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
|
||||||
|
|
||||||
const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload());
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(500);
|
|
||||||
|
|
||||||
mock.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should ignore ID in payload', async () => {
|
test('should ignore ID in payload', async () => {
|
||||||
const firstResponse = await authOwnerAgent
|
const firstResponse = await authOwnerAgent
|
||||||
.post('/credentials')
|
.post('/credentials')
|
||||||
|
@ -385,17 +372,6 @@ describe('PATCH /credentials/:id', () => {
|
||||||
|
|
||||||
expect(response.statusCode).toBe(404);
|
expect(response.statusCode).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with missing encryption key', async () => {
|
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
|
||||||
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
|
||||||
|
|
||||||
const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload());
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(500);
|
|
||||||
|
|
||||||
mock.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /credentials/new', () => {
|
describe('GET /credentials/new', () => {
|
||||||
|
@ -504,21 +480,6 @@ describe('GET /credentials/:id', () => {
|
||||||
expect(response.body.data).toBeUndefined(); // owner's cred not returned
|
expect(response.body.data).toBeUndefined(); // owner's cred not returned
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with missing encryption key', async () => {
|
|
||||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
|
||||||
|
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
|
||||||
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
|
||||||
|
|
||||||
const response = await authOwnerAgent
|
|
||||||
.get(`/credentials/${savedCredential.id}`)
|
|
||||||
.query({ includeData: true });
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(500);
|
|
||||||
|
|
||||||
mock.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return 404 if cred not found', async () => {
|
test('should return 404 if cred not found', async () => {
|
||||||
const response = await authOwnerAgent.get('/credentials/789');
|
const response = await authOwnerAgent.get('/credentials/789');
|
||||||
expect(response.statusCode).toBe(404);
|
expect(response.statusCode).toBe(404);
|
||||||
|
|
|
@ -89,7 +89,6 @@ beforeAll(async () => {
|
||||||
|
|
||||||
mockedSyslog.createClient.mockImplementation(() => new syslog.Client());
|
mockedSyslog.createClient.mockImplementation(() => new syslog.Client());
|
||||||
|
|
||||||
await utils.initEncryptionKey();
|
|
||||||
config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter');
|
config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter');
|
||||||
config.set('eventBus.logWriter.keepLogCount', 1);
|
config.set('eventBus.logWriter.keepLogCount', 1);
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,15 @@ import type { User } from '@db/entities/User';
|
||||||
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
|
||||||
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
||||||
import { LdapService } from '@/Ldap/LdapService.ee';
|
import { LdapService } from '@/Ldap/LdapService.ee';
|
||||||
import { encryptPassword, saveLdapSynchronization } from '@/Ldap/helpers';
|
import { saveLdapSynchronization } from '@/Ldap/helpers';
|
||||||
import type { LdapConfig } from '@/Ldap/types';
|
import type { LdapConfig } from '@/Ldap/types';
|
||||||
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
||||||
|
|
||||||
import { randomEmail, randomName, uniqueId } from './../shared/random';
|
import { randomEmail, randomName, uniqueId } from './../shared/random';
|
||||||
import * as testDb from './../shared/testDb';
|
import * as testDb from './../shared/testDb';
|
||||||
import * as utils from '../shared/utils/';
|
import * as utils from '../shared/utils/';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import { Cipher } from 'n8n-core';
|
||||||
|
|
||||||
jest.mock('@/telemetry');
|
jest.mock('@/telemetry');
|
||||||
|
|
||||||
|
@ -54,12 +56,10 @@ beforeAll(async () => {
|
||||||
owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
authOwnerAgent = testServer.authAgentFor(owner);
|
authOwnerAgent = testServer.authAgentFor(owner);
|
||||||
|
|
||||||
defaultLdapConfig.bindingAdminPassword = await encryptPassword(
|
defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(
|
||||||
defaultLdapConfig.bindingAdminPassword,
|
defaultLdapConfig.bindingAdminPassword,
|
||||||
);
|
);
|
||||||
|
|
||||||
await utils.initEncryptionKey();
|
|
||||||
|
|
||||||
await setCurrentAuthenticationMethod('email');
|
await setCurrentAuthenticationMethod('email');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,6 @@ const testServer = utils.setupTestServer({ endpointGroups: ['passwordReset'] });
|
||||||
const jwtService = Container.get(JwtService);
|
const jwtService = Container.get(JwtService);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.initEncryptionKey();
|
|
||||||
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||||
globalMemberRole = await testDb.getGlobalMemberRole();
|
globalMemberRole = await testDb.getGlobalMemberRole();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import type { SuperAgentTest } from 'supertest';
|
import type { SuperAgentTest } from 'supertest';
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
|
||||||
|
|
||||||
import { randomApiKey, randomName, randomString } from '../shared/random';
|
import { randomApiKey, randomName, randomString } from '../shared/random';
|
||||||
import * as utils from '../shared/utils/';
|
import * as utils from '../shared/utils/';
|
||||||
|
@ -22,9 +20,6 @@ let saveCredential: SaveCredentialFunction;
|
||||||
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
|
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// TODO: mock encryption key
|
|
||||||
await utils.initEncryptionKey();
|
|
||||||
|
|
||||||
const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] =
|
const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] =
|
||||||
await testDb.getAllRoles();
|
await testDb.getAllRoles();
|
||||||
|
|
||||||
|
@ -87,17 +82,6 @@ describe('POST /credentials', () => {
|
||||||
expect(response.statusCode === 400 || response.statusCode === 415).toBe(true);
|
expect(response.statusCode === 400 || response.statusCode === 415).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with missing encryption key', async () => {
|
|
||||||
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
|
|
||||||
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
|
|
||||||
|
|
||||||
const response = await authOwnerAgent.post('/credentials').send(credentialPayload());
|
|
||||||
|
|
||||||
expect(response.statusCode).toBe(500);
|
|
||||||
|
|
||||||
mock.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /credentials/:id', () => {
|
describe('DELETE /credentials/:id', () => {
|
||||||
|
|
|
@ -35,7 +35,6 @@ beforeAll(async () => {
|
||||||
apiKey: randomApiKey(),
|
apiKey: randomApiKey(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await utils.initEncryptionKey();
|
|
||||||
await utils.initNodeTypes();
|
await utils.initNodeTypes();
|
||||||
workflowRunner = await utils.initActiveWorkflowRunner();
|
workflowRunner = await utils.initActiveWorkflowRunner();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm';
|
import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm';
|
||||||
import { DataSource as Connection } from 'typeorm';
|
import { DataSource as Connection } from 'typeorm';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
@ -213,8 +212,6 @@ export async function createLdapUser(attributes: Partial<User>, ldapId: string):
|
||||||
export async function createUserWithMfaEnabled(
|
export async function createUserWithMfaEnabled(
|
||||||
data: { numberOfRecoveryCodes: number } = { numberOfRecoveryCodes: 10 },
|
data: { numberOfRecoveryCodes: number } = { numberOfRecoveryCodes: 10 },
|
||||||
) {
|
) {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
const email = randomEmail();
|
const email = randomEmail();
|
||||||
const password = randomPassword();
|
const password = randomPassword();
|
||||||
|
|
||||||
|
@ -222,7 +219,7 @@ export async function createUserWithMfaEnabled(
|
||||||
|
|
||||||
const secret = toptService.generateSecret();
|
const secret = toptService.generateSecret();
|
||||||
|
|
||||||
const mfaService = new MfaService(Db.collections.User, toptService, encryptionKey);
|
const mfaService = Container.get(MfaService);
|
||||||
|
|
||||||
const recoveryCodes = mfaService.generateRecoveryCodes(data.numberOfRecoveryCodes);
|
const recoveryCodes = mfaService.generateRecoveryCodes(data.numberOfRecoveryCodes);
|
||||||
|
|
||||||
|
@ -687,12 +684,10 @@ const getDBOptions = (type: TestDBType, name: string) => ({
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
async function encryptCredentialData(credential: CredentialsEntity) {
|
async function encryptCredentialData(credential: CredentialsEntity) {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
const coreCredential = createCredentialsFromCredentialsEntity(credential, true);
|
const coreCredential = createCredentialsFromCredentialsEntity(credential, true);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
coreCredential.setData(credential.data, encryptionKey);
|
coreCredential.setData(credential.data);
|
||||||
|
|
||||||
return coreCredential.getDataToSave() as ICredentialsDb;
|
return coreCredential.getDataToSave() as ICredentialsDb;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { randomBytes } from 'crypto';
|
import { BinaryDataService } from 'n8n-core';
|
||||||
import { existsSync } from 'fs';
|
|
||||||
import { BinaryDataService, UserSettings } from 'n8n-core';
|
|
||||||
import type { INode } from 'n8n-workflow';
|
import type { INode } from 'n8n-workflow';
|
||||||
import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.credentials';
|
import { GithubApi } from 'n8n-nodes-base/credentials/GithubApi.credentials';
|
||||||
import { Ftp } from 'n8n-nodes-base/credentials/Ftp.credentials';
|
import { Ftp } from 'n8n-nodes-base/credentials/Ftp.credentials';
|
||||||
|
@ -84,19 +82,6 @@ export async function initBinaryDataService(mode: 'default' | 'filesystem' = 'de
|
||||||
Container.set(BinaryDataService, binaryDataService);
|
Container.set(BinaryDataService, binaryDataService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a user settings config file if non-existent.
|
|
||||||
*/
|
|
||||||
// TODO: this should be mocked
|
|
||||||
export async function initEncryptionKey() {
|
|
||||||
const settingsPath = UserSettings.getUserSettingsPath();
|
|
||||||
|
|
||||||
if (!existsSync(settingsPath)) {
|
|
||||||
const userSettings = { encryptionKey: randomBytes(24).toString('base64') };
|
|
||||||
await UserSettings.writeUserSettings(userSettings, settingsPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the value (token) of the auth cookie in a response.
|
* Extract the value (token) of the auth cookie in a response.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -7,7 +7,6 @@ import request from 'supertest';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
import { workflowsController } from '@/workflows/workflows.controller';
|
import { workflowsController } from '@/workflows/workflows.controller';
|
||||||
|
@ -50,8 +49,6 @@ import type { EndpointGroup, SetupProps, TestServer } from '../types';
|
||||||
import { mockInstance } from './mocking';
|
import { mockInstance } from './mocking';
|
||||||
import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
|
import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
|
||||||
import { MfaService } from '@/Mfa/mfa.service';
|
import { MfaService } from '@/Mfa/mfa.service';
|
||||||
import { TOTPService } from '@/Mfa/totp.service';
|
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import { MetricsService } from '@/services/metrics.service';
|
import { MetricsService } from '@/services/metrics.service';
|
||||||
import {
|
import {
|
||||||
SettingsRepository,
|
SettingsRepository,
|
||||||
|
@ -200,12 +197,10 @@ export const setupTestServer = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (functionEndpoints.length) {
|
if (functionEndpoints.length) {
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
const repositories = Db.collections;
|
|
||||||
const externalHooks = Container.get(ExternalHooks);
|
const externalHooks = Container.get(ExternalHooks);
|
||||||
const internalHooks = Container.get(InternalHooks);
|
const internalHooks = Container.get(InternalHooks);
|
||||||
const mailer = Container.get(UserManagementMailer);
|
const mailer = Container.get(UserManagementMailer);
|
||||||
const mfaService = new MfaService(repositories.User, new TOTPService(), encryptionKey);
|
const mfaService = Container.get(MfaService);
|
||||||
const userService = Container.get(UserService);
|
const userService = Container.get(UserService);
|
||||||
|
|
||||||
for (const group of functionEndpoints) {
|
for (const group of functionEndpoints) {
|
||||||
|
|
|
@ -16,7 +16,6 @@ const licenseLike = {
|
||||||
const testServer = utils.setupTestServer({ endpointGroups: ['variables'] });
|
const testServer = utils.setupTestServer({ endpointGroups: ['variables'] });
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.initEncryptionKey();
|
|
||||||
utils.mockInstance(License, licenseLike);
|
utils.mockInstance(License, licenseLike);
|
||||||
|
|
||||||
const owner = await testDb.createOwner();
|
const owner = await testDb.createOwner();
|
||||||
|
|
16
packages/cli/test/setup-test-folder.ts
Normal file
16
packages/cli/test/setup-test-folder.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
|
const baseDir = join(tmpdir(), 'n8n-tests/');
|
||||||
|
mkdirSync(baseDir, { recursive: true });
|
||||||
|
|
||||||
|
const testDir = mkdtempSync(baseDir);
|
||||||
|
mkdirSync(join(testDir, '.n8n'));
|
||||||
|
process.env.N8N_USER_FOLDER = testDir;
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(testDir, '.n8n/config'),
|
||||||
|
JSON.stringify({ encryptionKey: 'testkey', instanceId: '123' }),
|
||||||
|
'utf-8',
|
||||||
|
);
|
|
@ -12,6 +12,7 @@ import { CredentialsHelper } from '@/CredentialsHelper';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { mockInstance } from '../integration/shared/utils';
|
import { mockInstance } from '../integration/shared/utils';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
describe('CredentialsHelper', () => {
|
describe('CredentialsHelper', () => {
|
||||||
const TEST_ENCRYPTION_KEY = 'test';
|
const TEST_ENCRYPTION_KEY = 'test';
|
||||||
|
@ -277,7 +278,7 @@ describe('CredentialsHelper', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY);
|
const credentialsHelper = Container.get(CredentialsHelper);
|
||||||
|
|
||||||
const result = await credentialsHelper.authenticate(
|
const result = await credentialsHelper.authenticate(
|
||||||
testData.input.credentials,
|
testData.input.credentials,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import type { SettingsRepository } from '@/databases/repositories';
|
import { Container } from 'typedi';
|
||||||
|
import { Cipher } from 'n8n-core';
|
||||||
|
import { SettingsRepository } from '@/databases/repositories';
|
||||||
import type { ExternalSecretsSettings } from '@/Interfaces';
|
import type { ExternalSecretsSettings } from '@/Interfaces';
|
||||||
import { License } from '@/License';
|
import { License } from '@/License';
|
||||||
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
|
import { ExternalSecretsManager } from '@/ExternalSecrets/ExternalSecretsManager.ee';
|
||||||
import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee';
|
import { ExternalSecretsProviders } from '@/ExternalSecrets/ExternalSecretsProviders.ee';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import Container from 'typedi';
|
|
||||||
import { mockInstance } from '../../integration/shared/utils';
|
import { mockInstance } from '../../integration/shared/utils';
|
||||||
import {
|
import {
|
||||||
DummyProvider,
|
DummyProvider,
|
||||||
|
@ -13,56 +13,42 @@ import {
|
||||||
FailedProvider,
|
FailedProvider,
|
||||||
MockProviders,
|
MockProviders,
|
||||||
} from '../../shared/ExternalSecrets/utils';
|
} from '../../shared/ExternalSecrets/utils';
|
||||||
import { AES, enc } from 'crypto-js';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
|
||||||
|
|
||||||
const connectedDate = '2023-08-01T12:32:29.000Z';
|
|
||||||
const encryptionKey = 'testkey';
|
|
||||||
let settings: string | null = null;
|
|
||||||
const mockProvidersInstance = new MockProviders();
|
|
||||||
const settingsRepo = mock<SettingsRepository>({
|
|
||||||
async getEncryptedSecretsProviderSettings() {
|
|
||||||
return settings;
|
|
||||||
},
|
|
||||||
async saveEncryptedSecretsProviderSettings(data) {
|
|
||||||
settings = data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let licenseMock: License;
|
|
||||||
let providersMock: ExternalSecretsProviders;
|
|
||||||
let manager: ExternalSecretsManager | undefined;
|
|
||||||
|
|
||||||
const createMockSettings = (settings: ExternalSecretsSettings): string => {
|
|
||||||
return AES.encrypt(JSON.stringify(settings), encryptionKey).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const decryptSettings = (settings: string) => {
|
|
||||||
return JSON.parse(AES.decrypt(settings ?? '', encryptionKey).toString(enc.Utf8));
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('External Secrets Manager', () => {
|
describe('External Secrets Manager', () => {
|
||||||
|
const connectedDate = '2023-08-01T12:32:29.000Z';
|
||||||
|
let settings: string | null = null;
|
||||||
|
|
||||||
|
const mockProvidersInstance = new MockProviders();
|
||||||
|
const license = mockInstance(License);
|
||||||
|
const settingsRepo = mockInstance(SettingsRepository);
|
||||||
|
mockInstance(InternalHooks);
|
||||||
|
const cipher = Container.get(Cipher);
|
||||||
|
|
||||||
|
let providersMock: ExternalSecretsProviders;
|
||||||
|
let manager: ExternalSecretsManager;
|
||||||
|
|
||||||
|
const createMockSettings = (settings: ExternalSecretsSettings): string => {
|
||||||
|
return cipher.encrypt(settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptSettings = (settings: string) => {
|
||||||
|
return JSON.parse(cipher.decrypt(settings));
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest
|
|
||||||
.spyOn(UserSettings, 'getEncryptionKey')
|
|
||||||
.mockReturnValue(new Promise((resolve) => resolve(encryptionKey)));
|
|
||||||
providersMock = mockInstance(ExternalSecretsProviders, mockProvidersInstance);
|
providersMock = mockInstance(ExternalSecretsProviders, mockProvidersInstance);
|
||||||
licenseMock = mockInstance(License, {
|
settings = createMockSettings({
|
||||||
isExternalSecretsEnabled() {
|
dummy: { connected: true, connectedAt: new Date(connectedDate), settings: {} },
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
mockInstance(InternalHooks);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockProvidersInstance.setProviders({
|
mockProvidersInstance.setProviders({
|
||||||
dummy: DummyProvider,
|
dummy: DummyProvider,
|
||||||
});
|
});
|
||||||
settings = createMockSettings({
|
license.isExternalSecretsEnabled.mockReturnValue(true);
|
||||||
dummy: { connected: true, connectedAt: new Date(connectedDate), settings: {} },
|
settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
|
||||||
});
|
manager = new ExternalSecretsManager(settingsRepo, license, providersMock, cipher);
|
||||||
|
|
||||||
Container.remove(ExternalSecretsManager);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -71,8 +57,6 @@ describe('External Secrets Manager', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should get secret', async () => {
|
test('should get secret', async () => {
|
||||||
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
|
|
||||||
|
|
||||||
await manager.init();
|
await manager.init();
|
||||||
|
|
||||||
expect(manager.getSecret('dummy', 'test1')).toBe('value1');
|
expect(manager.getSecret('dummy', 'test1')).toBe('value1');
|
||||||
|
@ -82,8 +66,6 @@ describe('External Secrets Manager', () => {
|
||||||
mockProvidersInstance.setProviders({
|
mockProvidersInstance.setProviders({
|
||||||
dummy: ErrorProvider,
|
dummy: ErrorProvider,
|
||||||
});
|
});
|
||||||
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
|
|
||||||
|
|
||||||
expect(async () => manager!.init()).not.toThrow();
|
expect(async () => manager!.init()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -91,16 +73,12 @@ describe('External Secrets Manager', () => {
|
||||||
mockProvidersInstance.setProviders({
|
mockProvidersInstance.setProviders({
|
||||||
dummy: ErrorProvider,
|
dummy: ErrorProvider,
|
||||||
});
|
});
|
||||||
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
|
|
||||||
|
|
||||||
await manager.init();
|
await manager.init();
|
||||||
expect(() => manager!.shutdown()).not.toThrow();
|
expect(() => manager!.shutdown()).not.toThrow();
|
||||||
manager = undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should save provider settings', async () => {
|
test('should save provider settings', async () => {
|
||||||
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
|
|
||||||
|
|
||||||
const settingsSpy = jest.spyOn(settingsRepo, 'saveEncryptedSecretsProviderSettings');
|
const settingsSpy = jest.spyOn(settingsRepo, 'saveEncryptedSecretsProviderSettings');
|
||||||
|
|
||||||
await manager.init();
|
await manager.init();
|
||||||
|
@ -122,8 +100,6 @@ describe('External Secrets Manager', () => {
|
||||||
|
|
||||||
test('should call provider update functions on a timer', async () => {
|
test('should call provider update functions on a timer', async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
|
|
||||||
|
|
||||||
await manager.init();
|
await manager.init();
|
||||||
|
|
||||||
const updateSpy = jest.spyOn(manager.getProvider('dummy')!, 'update');
|
const updateSpy = jest.spyOn(manager.getProvider('dummy')!, 'update');
|
||||||
|
@ -138,15 +114,7 @@ describe('External Secrets Manager', () => {
|
||||||
test('should not call provider update functions if the not licensed', async () => {
|
test('should not call provider update functions if the not licensed', async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
manager = new ExternalSecretsManager(
|
license.isExternalSecretsEnabled.mockReturnValue(false);
|
||||||
settingsRepo,
|
|
||||||
mock<License>({
|
|
||||||
isExternalSecretsEnabled() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
providersMock,
|
|
||||||
);
|
|
||||||
|
|
||||||
await manager.init();
|
await manager.init();
|
||||||
|
|
||||||
|
@ -165,7 +133,6 @@ describe('External Secrets Manager', () => {
|
||||||
mockProvidersInstance.setProviders({
|
mockProvidersInstance.setProviders({
|
||||||
dummy: FailedProvider,
|
dummy: FailedProvider,
|
||||||
});
|
});
|
||||||
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
|
|
||||||
|
|
||||||
await manager.init();
|
await manager.init();
|
||||||
|
|
||||||
|
@ -179,8 +146,6 @@ describe('External Secrets Manager', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reinitialize a provider when save provider settings', async () => {
|
test('should reinitialize a provider when save provider settings', async () => {
|
||||||
manager = new ExternalSecretsManager(settingsRepo, licenseMock, providersMock);
|
|
||||||
|
|
||||||
await manager.init();
|
await manager.init();
|
||||||
|
|
||||||
const dummyInitSpy = jest.spyOn(DummyProvider.prototype, 'init');
|
const dummyInitSpy = jest.spyOn(DummyProvider.prototype, 'init');
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { LicenseManager } from '@n8n_io/license-sdk';
|
import { LicenseManager } from '@n8n_io/license-sdk';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { License } from '@/License';
|
import { License } from '@/License';
|
||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
import { mockInstance } from '../integration/shared/utils';
|
||||||
|
|
||||||
jest.mock('@n8n_io/license-sdk');
|
jest.mock('@n8n_io/license-sdk');
|
||||||
|
|
||||||
|
@ -21,10 +23,11 @@ describe('License', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
let license: License;
|
let license: License;
|
||||||
|
const instanceSettings = mockInstance(InstanceSettings, { instanceId: MOCK_INSTANCE_ID });
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
license = new License();
|
license = new License(instanceSettings);
|
||||||
await license.init(MOCK_INSTANCE_ID);
|
await license.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initializes license manager', async () => {
|
test('initializes license manager', async () => {
|
||||||
|
@ -45,8 +48,8 @@ describe('License', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initializes license manager for worker', async () => {
|
test('initializes license manager for worker', async () => {
|
||||||
license = new License();
|
license = new License(instanceSettings);
|
||||||
await license.init(MOCK_INSTANCE_ID, 'worker');
|
await license.init('worker');
|
||||||
expect(LicenseManager).toHaveBeenCalledWith({
|
expect(LicenseManager).toHaveBeenCalledWith({
|
||||||
autoRenewEnabled: false,
|
autoRenewEnabled: false,
|
||||||
autoRenewOffset: MOCK_RENEW_OFFSET,
|
autoRenewOffset: MOCK_RENEW_OFFSET,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { PostHog } from 'posthog-node';
|
import { PostHog } from 'posthog-node';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import { mockInstance } from '../integration/shared/utils';
|
||||||
|
|
||||||
jest.mock('posthog-node');
|
jest.mock('posthog-node');
|
||||||
|
|
||||||
|
@ -10,6 +12,8 @@ describe('PostHog', () => {
|
||||||
const apiKey = 'api-key';
|
const apiKey = 'api-key';
|
||||||
const apiHost = 'api-host';
|
const apiHost = 'api-host';
|
||||||
|
|
||||||
|
const instanceSettings = mockInstance(InstanceSettings, { instanceId });
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
config.set('diagnostics.config.posthog.apiKey', apiKey);
|
config.set('diagnostics.config.posthog.apiKey', apiKey);
|
||||||
config.set('diagnostics.config.posthog.apiHost', apiHost);
|
config.set('diagnostics.config.posthog.apiHost', apiHost);
|
||||||
|
@ -21,8 +25,8 @@ describe('PostHog', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('inits PostHog correctly', async () => {
|
it('inits PostHog correctly', async () => {
|
||||||
const ph = new PostHogClient();
|
const ph = new PostHogClient(instanceSettings);
|
||||||
await ph.init(instanceId);
|
await ph.init();
|
||||||
|
|
||||||
expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, { host: apiHost });
|
expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, { host: apiHost });
|
||||||
});
|
});
|
||||||
|
@ -30,8 +34,8 @@ describe('PostHog', () => {
|
||||||
it('does not initialize or track if diagnostics are not enabled', async () => {
|
it('does not initialize or track if diagnostics are not enabled', async () => {
|
||||||
config.set('diagnostics.enabled', false);
|
config.set('diagnostics.enabled', false);
|
||||||
|
|
||||||
const ph = new PostHogClient();
|
const ph = new PostHogClient(instanceSettings);
|
||||||
await ph.init(instanceId);
|
await ph.init();
|
||||||
|
|
||||||
ph.track({
|
ph.track({
|
||||||
userId: 'test',
|
userId: 'test',
|
||||||
|
@ -50,8 +54,8 @@ describe('PostHog', () => {
|
||||||
test: true,
|
test: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ph = new PostHogClient();
|
const ph = new PostHogClient(instanceSettings);
|
||||||
await ph.init(instanceId);
|
await ph.init();
|
||||||
|
|
||||||
ph.track({
|
ph.track({
|
||||||
userId,
|
userId,
|
||||||
|
@ -70,8 +74,8 @@ describe('PostHog', () => {
|
||||||
|
|
||||||
it('gets feature flags', async () => {
|
it('gets feature flags', async () => {
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
const ph = new PostHogClient();
|
const ph = new PostHogClient(instanceSettings);
|
||||||
await ph.init(instanceId);
|
await ph.init();
|
||||||
|
|
||||||
await ph.getFeatureFlags({
|
await ph.getFeatureFlags({
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|
|
@ -9,12 +9,11 @@ import {
|
||||||
} from '@/environments/sourceControl/sourceControlHelper.ee';
|
} from '@/environments/sourceControl/sourceControlHelper.ee';
|
||||||
import { License } from '@/License';
|
import { License } from '@/License';
|
||||||
import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee';
|
import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import {
|
||||||
SOURCE_CONTROL_SSH_FOLDER,
|
SOURCE_CONTROL_SSH_FOLDER,
|
||||||
SOURCE_CONTROL_GIT_FOLDER,
|
SOURCE_CONTROL_GIT_FOLDER,
|
||||||
SOURCE_CONTROL_SSH_KEY_NAME,
|
|
||||||
} from '@/environments/sourceControl/constants';
|
} from '@/environments/sourceControl/constants';
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
|
@ -184,10 +183,9 @@ describe('Source Control', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should check for git and ssh folders and create them if required', async () => {
|
it('should check for git and ssh folders and create them if required', async () => {
|
||||||
const userFolder = UserSettings.getUserN8nFolderPath();
|
const { n8nFolder } = Container.get(InstanceSettings);
|
||||||
const sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER);
|
const sshFolder = path.join(n8nFolder, SOURCE_CONTROL_SSH_FOLDER);
|
||||||
const gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
|
const gitFolder = path.join(n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
|
||||||
const sshKeyName = path.join(sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
|
|
||||||
let hasThrown = false;
|
let hasThrown = false;
|
||||||
try {
|
try {
|
||||||
accessSync(sshFolder, fsConstants.F_OK);
|
accessSync(sshFolder, fsConstants.F_OK);
|
||||||
|
|
|
@ -4,6 +4,8 @@ import config from '@/config';
|
||||||
import { flushPromises } from './Helpers';
|
import { flushPromises } from './Helpers';
|
||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { mockInstance } from '../integration/shared/utils';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
jest.unmock('@/telemetry');
|
jest.unmock('@/telemetry');
|
||||||
jest.mock('@/license/License.service', () => {
|
jest.mock('@/license/License.service', () => {
|
||||||
|
@ -28,6 +30,7 @@ describe('Telemetry', () => {
|
||||||
let telemetry: Telemetry;
|
let telemetry: Telemetry;
|
||||||
const instanceId = 'Telemetry unit test';
|
const instanceId = 'Telemetry unit test';
|
||||||
const testDateTime = new Date('2022-01-01 00:00:00');
|
const testDateTime = new Date('2022-01-01 00:00:00');
|
||||||
|
const instanceSettings = mockInstance(InstanceSettings, { instanceId });
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
startPulseSpy = jest
|
startPulseSpy = jest
|
||||||
|
@ -49,11 +52,10 @@ describe('Telemetry', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
spyTrack.mockClear();
|
spyTrack.mockClear();
|
||||||
|
|
||||||
const postHog = new PostHogClient();
|
const postHog = new PostHogClient(instanceSettings);
|
||||||
await postHog.init(instanceId);
|
await postHog.init();
|
||||||
|
|
||||||
telemetry = new Telemetry(postHog, mock());
|
telemetry = new Telemetry(postHog, mock(), instanceSettings);
|
||||||
telemetry.setInstanceId(instanceId);
|
|
||||||
(telemetry as any).rudderStack = mockRudderStack;
|
(telemetry as any).rudderStack = mockRudderStack;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
"@types/aws4": "^1.5.1",
|
"@types/aws4": "^1.5.1",
|
||||||
"@types/concat-stream": "^2.0.0",
|
"@types/concat-stream": "^2.0.0",
|
||||||
"@types/cron": "~1.7.1",
|
"@types/cron": "~1.7.1",
|
||||||
"@types/crypto-js": "^4.0.1",
|
"@types/crypto-js": "^4.1.3",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/lodash": "^4.14.195",
|
"@types/lodash": "^4.14.195",
|
||||||
"@types/mime-types": "^2.1.0",
|
"@types/mime-types": "^2.1.0",
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"concat-stream": "^2.0.0",
|
"concat-stream": "^2.0.0",
|
||||||
"cron": "~1.7.2",
|
"cron": "~1.7.2",
|
||||||
"crypto-js": "~4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"fast-glob": "^3.2.5",
|
"fast-glob": "^3.2.5",
|
||||||
"file-type": "^16.5.4",
|
"file-type": "^16.5.4",
|
||||||
"flatted": "^3.2.4",
|
"flatted": "^3.2.4",
|
||||||
|
|
19
packages/core/src/Cipher.ts
Normal file
19
packages/core/src/Cipher.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
import { AES, enc } from 'crypto-js';
|
||||||
|
import { InstanceSettings } from './InstanceSettings';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class Cipher {
|
||||||
|
constructor(private readonly instanceSettings: InstanceSettings) {}
|
||||||
|
|
||||||
|
encrypt(data: string | object) {
|
||||||
|
return AES.encrypt(
|
||||||
|
typeof data === 'string' ? data : JSON.stringify(data),
|
||||||
|
this.instanceSettings.encryptionKey,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(data: string) {
|
||||||
|
return AES.decrypt(data, this.instanceSettings.encryptionKey).toString(enc.Utf8);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,6 @@
|
||||||
export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS';
|
export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS';
|
||||||
export const DOWNLOADED_NODES_SUBDIRECTORY = 'nodes';
|
|
||||||
export const ENCRYPTION_KEY_ENV_OVERWRITE = 'N8N_ENCRYPTION_KEY';
|
|
||||||
export const EXTENSIONS_SUBDIRECTORY = 'custom';
|
|
||||||
export const USER_FOLDER_ENV_OVERWRITE = 'N8N_USER_FOLDER';
|
|
||||||
export const USER_SETTINGS_FILE_NAME = 'config';
|
|
||||||
export const USER_SETTINGS_SUBFOLDER = '.n8n';
|
|
||||||
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__';
|
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__';
|
||||||
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
||||||
export const TUNNEL_SUBDOMAIN_ENV = 'N8N_TUNNEL_SUBDOMAIN';
|
|
||||||
|
|
||||||
export const RESPONSE_ERROR_MESSAGES = {
|
|
||||||
NO_ENCRYPTION_KEY: 'Encryption key is missing or was not set',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import type {
|
import { Container } from 'typedi';
|
||||||
CredentialInformation,
|
import type { ICredentialDataDecryptedObject, ICredentialsEncrypted } from 'n8n-workflow';
|
||||||
ICredentialDataDecryptedObject,
|
import { ICredentials, jsonParse } from 'n8n-workflow';
|
||||||
ICredentialsEncrypted,
|
import { Cipher } from './Cipher';
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { ICredentials } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { AES, enc } from 'crypto-js';
|
|
||||||
|
|
||||||
export class Credentials extends ICredentials {
|
export class Credentials extends ICredentials {
|
||||||
|
private readonly cipher = Container.get(Cipher);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the given nodeType has access to data
|
* Returns if the given nodeType has access to data
|
||||||
*/
|
*/
|
||||||
|
@ -24,30 +22,14 @@ export class Credentials extends ICredentials {
|
||||||
/**
|
/**
|
||||||
* Sets new credential object
|
* Sets new credential object
|
||||||
*/
|
*/
|
||||||
setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void {
|
setData(data: ICredentialDataDecryptedObject): void {
|
||||||
this.data = AES.encrypt(JSON.stringify(data), encryptionKey).toString();
|
this.data = this.cipher.encrypt(data);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets new credentials for given key
|
|
||||||
*/
|
|
||||||
setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void {
|
|
||||||
let fullData;
|
|
||||||
try {
|
|
||||||
fullData = this.getData(encryptionKey);
|
|
||||||
} catch (e) {
|
|
||||||
fullData = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
fullData[key] = data;
|
|
||||||
|
|
||||||
return this.setData(fullData, encryptionKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the decrypted credential object
|
* Returns the decrypted credential object
|
||||||
*/
|
*/
|
||||||
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
|
getData(nodeType?: string): ICredentialDataDecryptedObject {
|
||||||
if (nodeType && !this.hasNodeAccess(nodeType)) {
|
if (nodeType && !this.hasNodeAccess(nodeType)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`,
|
`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`,
|
||||||
|
@ -58,11 +40,10 @@ export class Credentials extends ICredentials {
|
||||||
throw new Error('No data is set so nothing can be returned.');
|
throw new Error('No data is set so nothing can be returned.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedData = AES.decrypt(this.data, encryptionKey);
|
const decryptedData = this.cipher.decrypt(this.data);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
return jsonParse(decryptedData);
|
||||||
return JSON.parse(decryptedData.toString(enc.Utf8));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
|
'Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
|
||||||
|
@ -70,23 +51,6 @@ export class Credentials extends ICredentials {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the decrypted credentials for given key
|
|
||||||
*/
|
|
||||||
getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation {
|
|
||||||
const fullData = this.getData(encryptionKey, nodeType);
|
|
||||||
|
|
||||||
if (fullData === null) {
|
|
||||||
throw new Error('No data was set.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fullData.hasOwnProperty(key)) {
|
|
||||||
throw new Error(`No data for key "${key}" exists.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullData[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the encrypted credentials to be saved
|
* Returns the encrypted credentials to be saved
|
||||||
*/
|
*/
|
||||||
|
|
86
packages/core/src/InstanceSettings.ts
Normal file
86
packages/core/src/InstanceSettings.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import path from 'path';
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { createHash, randomBytes } from 'crypto';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
|
||||||
|
interface ReadOnlySettings {
|
||||||
|
encryptionKey: string;
|
||||||
|
instanceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WritableSettings {
|
||||||
|
tunnelSubdomain?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Settings = ReadOnlySettings & WritableSettings;
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class InstanceSettings {
|
||||||
|
readonly userHome = this.getUserHome();
|
||||||
|
|
||||||
|
/** The path to the n8n folder in which all n8n related data gets saved */
|
||||||
|
readonly n8nFolder = path.join(this.userHome, '.n8n');
|
||||||
|
|
||||||
|
/** The path to the folder containing custom nodes and credentials */
|
||||||
|
readonly customExtensionDir = path.join(this.n8nFolder, 'custom');
|
||||||
|
|
||||||
|
/** The path to the folder containing installed nodes (like community nodes) */
|
||||||
|
readonly nodesDownloadDir = path.join(this.n8nFolder, 'nodes');
|
||||||
|
|
||||||
|
private readonly settingsFile = path.join(this.n8nFolder, 'config');
|
||||||
|
|
||||||
|
private settings = this.loadOrCreate();
|
||||||
|
|
||||||
|
get encryptionKey() {
|
||||||
|
return this.settings.encryptionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get instanceId() {
|
||||||
|
return this.settings.instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tunnelSubdomain() {
|
||||||
|
return this.settings.tunnelSubdomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(newSettings: WritableSettings) {
|
||||||
|
this.save({ ...this.settings, ...newSettings });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The home folder path of the user.
|
||||||
|
* If none can be found it falls back to the current working directory
|
||||||
|
*/
|
||||||
|
private getUserHome() {
|
||||||
|
const homeVarName = process.platform === 'win32' ? 'USERPROFILE' : 'HOME';
|
||||||
|
return process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadOrCreate(): Settings {
|
||||||
|
const { settingsFile } = this;
|
||||||
|
if (existsSync(settingsFile)) {
|
||||||
|
const content = readFileSync(settingsFile, 'utf8');
|
||||||
|
return jsonParse(content, {
|
||||||
|
errorMessage: `Error parsing n8n-config file "${settingsFile}". It does not seem to be valid JSON.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file doesn't exist, create new settings
|
||||||
|
const encryptionKey = process.env.N8N_ENCRYPTION_KEY ?? randomBytes(24).toString('base64');
|
||||||
|
const instanceId = createHash('sha256')
|
||||||
|
.update(encryptionKey.slice(Math.round(encryptionKey.length / 2)))
|
||||||
|
.digest('hex');
|
||||||
|
|
||||||
|
const settings = { encryptionKey, instanceId };
|
||||||
|
mkdirSync(path.dirname(settingsFile));
|
||||||
|
this.save(settings);
|
||||||
|
console.log(`UserSettings were generated and saved to: ${settingsFile}`);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private save(settings: Settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
writeFileSync(this.settingsFile, JSON.stringify(settings, null, '\t'), 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,12 +15,6 @@ export interface IResponseError extends Error {
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserSettings {
|
|
||||||
encryptionKey?: string;
|
|
||||||
tunnelSubdomain?: string;
|
|
||||||
instanceId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||||
errorWorkflow?: string;
|
errorWorkflow?: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
|
|
|
@ -3,14 +3,11 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-shadow */
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ClientOAuth2Options,
|
ClientOAuth2Options,
|
||||||
ClientOAuth2RequestObject,
|
ClientOAuth2RequestObject,
|
||||||
|
@ -143,9 +140,9 @@ import {
|
||||||
setWorkflowExecutionMetadata,
|
setWorkflowExecutionMetadata,
|
||||||
} from './WorkflowExecutionMetadata';
|
} from './WorkflowExecutionMetadata';
|
||||||
import { getSecretsProxy } from './Secrets';
|
import { getSecretsProxy } from './Secrets';
|
||||||
import { getUserN8nFolderPath, getInstanceId } from './UserSettings';
|
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import type { BinaryData } from './BinaryData/types';
|
import type { BinaryData } from './BinaryData/types';
|
||||||
|
import { InstanceSettings } from './InstanceSettings';
|
||||||
|
|
||||||
axios.defaults.timeout = 300000;
|
axios.defaults.timeout = 300000;
|
||||||
// Prevent axios from adding x-form-www-urlencoded headers by default
|
// Prevent axios from adding x-form-www-urlencoded headers by default
|
||||||
|
@ -2510,7 +2507,7 @@ const getCommonWorkflowFunctions = (
|
||||||
|
|
||||||
getRestApiUrl: () => additionalData.restApiUrl,
|
getRestApiUrl: () => additionalData.restApiUrl,
|
||||||
getInstanceBaseUrl: () => additionalData.instanceBaseUrl,
|
getInstanceBaseUrl: () => additionalData.instanceBaseUrl,
|
||||||
getInstanceId: async () => getInstanceId(),
|
getInstanceId: () => Container.get(InstanceSettings).instanceId,
|
||||||
getTimezone: () => getTimezone(workflow, additionalData),
|
getTimezone: () => getTimezone(workflow, additionalData),
|
||||||
|
|
||||||
prepareOutputData: async (outputData) => [outputData],
|
prepareOutputData: async (outputData) => [outputData],
|
||||||
|
@ -2600,7 +2597,6 @@ const getAllowedPaths = () => {
|
||||||
function isFilePathBlocked(filePath: string): boolean {
|
function isFilePathBlocked(filePath: string): boolean {
|
||||||
const allowedPaths = getAllowedPaths();
|
const allowedPaths = getAllowedPaths();
|
||||||
const resolvedFilePath = path.resolve(filePath);
|
const resolvedFilePath = path.resolve(filePath);
|
||||||
const userFolder = getUserN8nFolderPath();
|
|
||||||
const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false';
|
const blockFileAccessToN8nFiles = process.env[BLOCK_FILE_ACCESS_TO_N8N_FILES] !== 'false';
|
||||||
|
|
||||||
//if allowed paths are defined, allow access only to those paths
|
//if allowed paths are defined, allow access only to those paths
|
||||||
|
@ -2616,7 +2612,8 @@ function isFilePathBlocked(filePath: string): boolean {
|
||||||
|
|
||||||
//restrict access to .n8n folder and other .env config related paths
|
//restrict access to .n8n folder and other .env config related paths
|
||||||
if (blockFileAccessToN8nFiles) {
|
if (blockFileAccessToN8nFiles) {
|
||||||
const restrictedPaths: string[] = [userFolder];
|
const { n8nFolder } = Container.get(InstanceSettings);
|
||||||
|
const restrictedPaths = [n8nFolder];
|
||||||
|
|
||||||
if (process.env[CONFIG_FILES]) {
|
if (process.env[CONFIG_FILES]) {
|
||||||
restrictedPaths.push(...process.env[CONFIG_FILES].split(','));
|
restrictedPaths.push(...process.env[CONFIG_FILES].split(','));
|
||||||
|
@ -2674,7 +2671,7 @@ const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunctions =>
|
||||||
},
|
},
|
||||||
|
|
||||||
getStoragePath() {
|
getStoragePath() {
|
||||||
return path.join(getUserN8nFolderPath(), `storage/${node.type}`);
|
return path.join(Container.get(InstanceSettings).n8nFolder, `storage/${node.type}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async writeContentToFile(filePath, content, flag) {
|
async writeContentToFile(filePath, content, flag) {
|
||||||
|
|
|
@ -1,272 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { createHash, randomBytes } from 'crypto';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import { deepCopy } from 'n8n-workflow';
|
|
||||||
import {
|
|
||||||
ENCRYPTION_KEY_ENV_OVERWRITE,
|
|
||||||
EXTENSIONS_SUBDIRECTORY,
|
|
||||||
DOWNLOADED_NODES_SUBDIRECTORY,
|
|
||||||
RESPONSE_ERROR_MESSAGES,
|
|
||||||
USER_FOLDER_ENV_OVERWRITE,
|
|
||||||
USER_SETTINGS_FILE_NAME,
|
|
||||||
USER_SETTINGS_SUBFOLDER,
|
|
||||||
} from './Constants';
|
|
||||||
import type { IUserSettings } from './Interfaces';
|
|
||||||
|
|
||||||
const fsAccess = promisify(fs.access);
|
|
||||||
const fsReadFile = promisify(fs.readFile);
|
|
||||||
const fsMkdir = promisify(fs.mkdir);
|
|
||||||
const fsWriteFile = promisify(fs.writeFile);
|
|
||||||
|
|
||||||
let settingsCache: IUserSettings | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the user settings if they do not exist yet
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export async function prepareUserSettings(): Promise<IUserSettings> {
|
|
||||||
const settingsPath = getUserSettingsPath();
|
|
||||||
|
|
||||||
let userSettings = await getUserSettings(settingsPath);
|
|
||||||
if (userSettings !== undefined) {
|
|
||||||
// Settings already exist, check if they contain the encryptionKey
|
|
||||||
if (userSettings.encryptionKey !== undefined) {
|
|
||||||
// Key already exists
|
|
||||||
if (userSettings.instanceId === undefined) {
|
|
||||||
userSettings.instanceId = await generateInstanceId(userSettings.encryptionKey);
|
|
||||||
settingsCache = userSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userSettings;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
userSettings = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
|
||||||
// Use the encryption key which got set via environment
|
|
||||||
userSettings.encryptionKey = process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
|
|
||||||
} else {
|
|
||||||
// Generate a new encryption key
|
|
||||||
userSettings.encryptionKey = randomBytes(24).toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
userSettings.instanceId = await generateInstanceId(userSettings.encryptionKey);
|
|
||||||
|
|
||||||
console.log(`UserSettings were generated and saved to: ${settingsPath}`);
|
|
||||||
|
|
||||||
return writeUserSettings(userSettings, settingsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the encryption key which is used to encrypt
|
|
||||||
* the credentials.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function getEncryptionKey(): Promise<string> {
|
|
||||||
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
|
||||||
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
|
|
||||||
}
|
|
||||||
|
|
||||||
const userSettings = await getUserSettings();
|
|
||||||
|
|
||||||
if (userSettings?.encryptionKey === undefined) {
|
|
||||||
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return userSettings.encryptionKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the instance ID
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export async function getInstanceId(): Promise<string> {
|
|
||||||
const userSettings = await getUserSettings();
|
|
||||||
|
|
||||||
if (userSettings === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings.instanceId === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return userSettings.instanceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateInstanceId(key?: string) {
|
|
||||||
const hash = key
|
|
||||||
? createHash('sha256')
|
|
||||||
.update(key.slice(Math.round(key.length / 2)))
|
|
||||||
.digest('hex')
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds/Overwrite the given settings in the currently
|
|
||||||
* saved user settings
|
|
||||||
*
|
|
||||||
* @param {IUserSettings} addSettings The settings to add/overwrite
|
|
||||||
* @param {string} [settingsPath] Optional settings file path
|
|
||||||
*/
|
|
||||||
export async function addToUserSettings(
|
|
||||||
addSettings: IUserSettings,
|
|
||||||
settingsPath?: string,
|
|
||||||
): Promise<IUserSettings> {
|
|
||||||
if (settingsPath === undefined) {
|
|
||||||
settingsPath = getUserSettingsPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
let userSettings = await getUserSettings(settingsPath);
|
|
||||||
|
|
||||||
if (userSettings === undefined) {
|
|
||||||
userSettings = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the settings
|
|
||||||
Object.assign(userSettings, addSettings);
|
|
||||||
|
|
||||||
return writeUserSettings(userSettings, settingsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a user settings file
|
|
||||||
*
|
|
||||||
* @param {IUserSettings} userSettings The settings to write
|
|
||||||
* @param {string} [settingsPath] Optional settings file path
|
|
||||||
*/
|
|
||||||
export async function writeUserSettings(
|
|
||||||
userSettings: IUserSettings,
|
|
||||||
settingsPath?: string,
|
|
||||||
): Promise<IUserSettings> {
|
|
||||||
if (settingsPath === undefined) {
|
|
||||||
settingsPath = getUserSettingsPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userSettings === undefined) {
|
|
||||||
userSettings = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if parent folder exists if not create it.
|
|
||||||
try {
|
|
||||||
await fsAccess(path.dirname(settingsPath));
|
|
||||||
} catch (error) {
|
|
||||||
// Parent folder does not exist so create
|
|
||||||
await fsMkdir(path.dirname(settingsPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingsToWrite = { ...userSettings };
|
|
||||||
if (settingsToWrite.instanceId !== undefined) {
|
|
||||||
delete settingsToWrite.instanceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fsWriteFile(settingsPath, JSON.stringify(settingsToWrite, null, '\t'));
|
|
||||||
settingsCache = deepCopy(userSettings);
|
|
||||||
|
|
||||||
return userSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the content of the user settings
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export async function getUserSettings(
|
|
||||||
settingsPath?: string,
|
|
||||||
ignoreCache?: boolean,
|
|
||||||
): Promise<IUserSettings | undefined> {
|
|
||||||
if (settingsCache !== undefined && ignoreCache !== true) {
|
|
||||||
return settingsCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settingsPath === undefined) {
|
|
||||||
settingsPath = getUserSettingsPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fsAccess(settingsPath);
|
|
||||||
} catch (error) {
|
|
||||||
// The file does not exist
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingsFile = await fsReadFile(settingsPath, 'utf8');
|
|
||||||
|
|
||||||
try {
|
|
||||||
settingsCache = JSON.parse(settingsFile);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(
|
|
||||||
`Error parsing n8n-config file "${settingsPath}". It does not seem to be valid JSON.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return settingsCache as IUserSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the user settings
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function getUserSettingsPath(): string {
|
|
||||||
const n8nFolder = getUserN8nFolderPath();
|
|
||||||
|
|
||||||
return path.join(n8nFolder, USER_SETTINGS_FILE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the n8n folder in which all n8n
|
|
||||||
* related data gets saved
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function getUserN8nFolderPath(): string {
|
|
||||||
return path.join(getUserHome(), USER_SETTINGS_SUBFOLDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the n8n user folder with the custom
|
|
||||||
* extensions like nodes and credentials
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function getUserN8nFolderCustomExtensionPath(): string {
|
|
||||||
return path.join(getUserN8nFolderPath(), EXTENSIONS_SUBDIRECTORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path to the n8n user folder with the nodes that
|
|
||||||
* have been downloaded
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function getUserN8nFolderDownloadedNodesPath(): string {
|
|
||||||
return path.join(getUserN8nFolderPath(), DOWNLOADED_NODES_SUBDIRECTORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the home folder path of the user if
|
|
||||||
* none can be found it falls back to the current
|
|
||||||
* working directory
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function getUserHome(): string {
|
|
||||||
if (process.env[USER_FOLDER_ENV_OVERWRITE] !== undefined) {
|
|
||||||
return process.env[USER_FOLDER_ENV_OVERWRITE];
|
|
||||||
} else {
|
|
||||||
let variableName = 'HOME';
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
variableName = 'USERPROFILE';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env[variableName] === undefined) {
|
|
||||||
// If for some reason the variable does not exist
|
|
||||||
// fall back to current folder
|
|
||||||
return process.cwd();
|
|
||||||
}
|
|
||||||
return process.env[variableName] as string;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +1,21 @@
|
||||||
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
||||||
import * as UserSettings from './UserSettings';
|
|
||||||
|
|
||||||
export * from './ActiveWorkflows';
|
export * from './ActiveWorkflows';
|
||||||
export * from './BinaryData/BinaryData.service';
|
export * from './BinaryData/BinaryData.service';
|
||||||
export * from './BinaryData/types';
|
export * from './BinaryData/types';
|
||||||
|
export { Cipher } from './Cipher';
|
||||||
export * from './ClassLoader';
|
export * from './ClassLoader';
|
||||||
export * from './Constants';
|
export * from './Constants';
|
||||||
export * from './Credentials';
|
export * from './Credentials';
|
||||||
export * from './DirectoryLoader';
|
export * from './DirectoryLoader';
|
||||||
export * from './Interfaces';
|
export * from './Interfaces';
|
||||||
|
export { InstanceSettings } from './InstanceSettings';
|
||||||
export * from './LoadMappingOptions';
|
export * from './LoadMappingOptions';
|
||||||
export * from './LoadNodeParameterOptions';
|
export * from './LoadNodeParameterOptions';
|
||||||
export * from './LoadNodeListSearch';
|
export * from './LoadNodeListSearch';
|
||||||
export * from './NodeExecuteFunctions';
|
export * from './NodeExecuteFunctions';
|
||||||
export * from './WorkflowExecute';
|
export * from './WorkflowExecute';
|
||||||
export { NodeExecuteFunctions, UserSettings };
|
export { NodeExecuteFunctions };
|
||||||
export * from './errors';
|
export * from './errors';
|
||||||
export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee';
|
export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee';
|
||||||
export { BinaryData } from './BinaryData/types';
|
export { BinaryData } from './BinaryData/types';
|
||||||
|
|
|
@ -1,23 +1,39 @@
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { CredentialInformation } from 'n8n-workflow';
|
||||||
|
import { Cipher } from '@/Cipher';
|
||||||
import { Credentials } from '@/Credentials';
|
import { Credentials } from '@/Credentials';
|
||||||
|
import type { InstanceSettings } from '@/InstanceSettings';
|
||||||
|
|
||||||
describe('Credentials', () => {
|
describe('Credentials', () => {
|
||||||
|
const cipher = new Cipher(mock<InstanceSettings>({ encryptionKey: 'password' }));
|
||||||
|
Container.set(Cipher, cipher);
|
||||||
|
|
||||||
|
const setDataKey = (credentials: Credentials, key: string, data: CredentialInformation) => {
|
||||||
|
let fullData;
|
||||||
|
try {
|
||||||
|
fullData = credentials.getData();
|
||||||
|
} catch (e) {
|
||||||
|
fullData = {};
|
||||||
|
}
|
||||||
|
fullData[key] = data;
|
||||||
|
return credentials.setData(fullData);
|
||||||
|
};
|
||||||
|
|
||||||
describe('without nodeType set', () => {
|
describe('without nodeType set', () => {
|
||||||
test('should be able to set and read key data without initial data set', () => {
|
test('should be able to set and read key data without initial data set', () => {
|
||||||
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []);
|
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', []);
|
||||||
|
|
||||||
const key = 'key1';
|
const key = 'key1';
|
||||||
const password = 'password';
|
|
||||||
// const nodeType = 'base.noOp';
|
|
||||||
const newData = 1234;
|
const newData = 1234;
|
||||||
|
|
||||||
credentials.setDataKey(key, newData, password);
|
setDataKey(credentials, key, newData);
|
||||||
|
|
||||||
expect(credentials.getDataKey(key, password)).toEqual(newData);
|
expect(credentials.getData()[key]).toEqual(newData);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to set and read key data with initial data set', () => {
|
test('should be able to set and read key data with initial data set', () => {
|
||||||
const key = 'key2';
|
const key = 'key2';
|
||||||
const password = 'password';
|
|
||||||
|
|
||||||
// Saved under "key1"
|
// Saved under "key1"
|
||||||
const initialData = 4321;
|
const initialData = 4321;
|
||||||
|
@ -33,11 +49,11 @@ describe('Credentials', () => {
|
||||||
const newData = 1234;
|
const newData = 1234;
|
||||||
|
|
||||||
// Set and read new data
|
// Set and read new data
|
||||||
credentials.setDataKey(key, newData, password);
|
setDataKey(credentials, key, newData);
|
||||||
expect(credentials.getDataKey(key, password)).toEqual(newData);
|
expect(credentials.getData()[key]).toEqual(newData);
|
||||||
|
|
||||||
// Read the data which got provided encrypted on init
|
// Read the data which got provided encrypted on init
|
||||||
expect(credentials.getDataKey('key1', password)).toEqual(initialData);
|
expect(credentials.getData().key1).toEqual(initialData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,19 +70,18 @@ describe('Credentials', () => {
|
||||||
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', nodeAccess);
|
const credentials = new Credentials({ id: null, name: 'testName' }, 'testType', nodeAccess);
|
||||||
|
|
||||||
const key = 'key1';
|
const key = 'key1';
|
||||||
const password = 'password';
|
|
||||||
const nodeType = 'base.noOp';
|
const nodeType = 'base.noOp';
|
||||||
const newData = 1234;
|
const newData = 1234;
|
||||||
|
|
||||||
credentials.setDataKey(key, newData, password);
|
setDataKey(credentials, key, newData);
|
||||||
|
|
||||||
// Should be able to read with nodeType which has access
|
// Should be able to read with nodeType which has access
|
||||||
expect(credentials.getDataKey(key, password, nodeType)).toEqual(newData);
|
expect(credentials.getData(nodeType)[key]).toEqual(newData);
|
||||||
|
|
||||||
// Should not be able to read with nodeType which does NOT have access
|
// Should not be able to read with nodeType which does NOT have access
|
||||||
// expect(credentials.getDataKey(key, password, 'base.otherNode')).toThrowError(Error);
|
// expect(credentials.getData('base.otherNode')[key]).toThrowError(Error);
|
||||||
try {
|
try {
|
||||||
credentials.getDataKey(key, password, 'base.otherNode');
|
credentials.getData('base.otherNode');
|
||||||
expect(true).toBe(false);
|
expect(true).toBe(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe(
|
expect(e.message).toBe(
|
||||||
|
|
|
@ -52,6 +52,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDecrypted(
|
async getDecrypted(
|
||||||
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
nodeCredentials: INodeCredentialsDetails,
|
nodeCredentials: INodeCredentialsDetails,
|
||||||
type: string,
|
type: string,
|
||||||
): Promise<ICredentialDataDecryptedObject> {
|
): Promise<ICredentialDataDecryptedObject> {
|
||||||
|
@ -128,15 +129,12 @@ export function WorkflowExecuteAdditionalData(
|
||||||
connections: {},
|
connections: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
return {
|
return {
|
||||||
credentialsHelper: new CredentialsHelper(''),
|
credentialsHelper: new CredentialsHelper(),
|
||||||
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
||||||
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo) => {},
|
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo) => {},
|
||||||
sendMessageToUI: (message: string) => {},
|
sendDataToUI: (message: string) => {},
|
||||||
restApiUrl: '',
|
restApiUrl: '',
|
||||||
encryptionKey: 'test',
|
|
||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
webhookBaseUrl: 'webhook',
|
webhookBaseUrl: 'webhook',
|
||||||
webhookWaitingBaseUrl: 'webhook-waiting',
|
webhookWaitingBaseUrl: 'webhook-waiting',
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { UserSettings } from 'n8n-core';
|
import { Container } from 'typedi';
|
||||||
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
|
|
||||||
import type { IBuildOptions } from '../src';
|
import type { IBuildOptions } from '../src';
|
||||||
|
@ -17,7 +18,9 @@ export class Build extends Command {
|
||||||
help: flags.help({ char: 'h' }),
|
help: flags.help({ char: 'h' }),
|
||||||
destination: flags.string({
|
destination: flags.string({
|
||||||
char: 'd',
|
char: 'd',
|
||||||
description: `The path to copy the compiles files to [default: ${UserSettings.getUserN8nFolderCustomExtensionPath()}]`,
|
description: `The path to copy the compiled files to [default: ${
|
||||||
|
Container.get(InstanceSettings).customExtensionDir
|
||||||
|
}]`,
|
||||||
}),
|
}),
|
||||||
watch: flags.boolean({
|
watch: flags.boolean({
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
"n8n-core": "workspace:*",
|
"n8n-core": "workspace:*",
|
||||||
"n8n-workflow": "workspace:*",
|
"n8n-workflow": "workspace:*",
|
||||||
"replace-in-file": "^6.0.0",
|
"replace-in-file": "^6.0.0",
|
||||||
"tmp-promise": "^3.0.3"
|
"tmp-promise": "^3.0.3",
|
||||||
|
"typedi": "^0.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,11 @@ import glob from 'fast-glob';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises';
|
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises';
|
||||||
import { join, dirname, resolve as resolvePath } from 'path';
|
import { join, dirname, resolve as resolvePath } from 'path';
|
||||||
|
import { Container } from 'typedi';
|
||||||
import { file as tmpFile } from 'tmp-promise';
|
import { file as tmpFile } from 'tmp-promise';
|
||||||
|
|
||||||
import { jsonParse } from 'n8n-workflow';
|
import { jsonParse } from 'n8n-workflow';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import type { IBuildOptions } from './Interfaces';
|
import type { IBuildOptions } from './Interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,7 +50,7 @@ export async function createCustomTsconfig() {
|
||||||
* @param {IBuildOptions} [options] Options to overwrite default behavior
|
* @param {IBuildOptions} [options] Options to overwrite default behavior
|
||||||
*/
|
*/
|
||||||
export async function buildFiles({
|
export async function buildFiles({
|
||||||
destinationFolder = UserSettings.getUserN8nFolderCustomExtensionPath(),
|
destinationFolder = Container.get(InstanceSettings).customExtensionDir,
|
||||||
watch,
|
watch,
|
||||||
}: IBuildOptions): Promise<string> {
|
}: IBuildOptions): Promise<string> {
|
||||||
const tscPath = join(dirname(require.resolve('typescript')), 'tsc');
|
const tscPath = join(dirname(require.resolve('typescript')), 'tsc');
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require('../../jest.config'),
|
...require('../../jest.config'),
|
||||||
collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'],
|
collectCoverageFrom: ['credentials/**/*.ts', 'nodes/**/*.ts', 'utils/**/*.ts'],
|
||||||
|
setupFilesAfterEnv: ['jest-expect-message', '<rootDir>/test/setup.ts'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -226,7 +226,7 @@ function configureTransport(credentials: IDataObject, options: EmailSendOptions)
|
||||||
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
const items = this.getInputData();
|
const items = this.getInputData();
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
const instanceId = await this.getInstanceId();
|
const instanceId = this.getInstanceId();
|
||||||
|
|
||||||
const returnData: INodeExecutionData[] = [];
|
const returnData: INodeExecutionData[] = [];
|
||||||
let item: INodeExecutionData;
|
let item: INodeExecutionData;
|
||||||
|
|
|
@ -229,7 +229,7 @@ export class FormTrigger implements INodeType {
|
||||||
if (webhookName === 'setup') {
|
if (webhookName === 'setup') {
|
||||||
const formTitle = this.getNodeParameter('formTitle', '') as string;
|
const formTitle = this.getNodeParameter('formTitle', '') as string;
|
||||||
const formDescription = this.getNodeParameter('formDescription', '') as string;
|
const formDescription = this.getNodeParameter('formDescription', '') as string;
|
||||||
const instanceId = await this.getInstanceId();
|
const instanceId = this.getInstanceId();
|
||||||
const { formSubmittedText } = this.getNodeParameter('options', {}) as IDataObject;
|
const { formSubmittedText } = this.getNodeParameter('options', {}) as IDataObject;
|
||||||
|
|
||||||
const data = prepareFormData(
|
const data = prepareFormData(
|
||||||
|
|
|
@ -206,7 +206,7 @@ export class GmailV2 implements INodeType {
|
||||||
const resource = this.getNodeParameter('resource', 0);
|
const resource = this.getNodeParameter('resource', 0);
|
||||||
const operation = this.getNodeParameter('operation', 0);
|
const operation = this.getNodeParameter('operation', 0);
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
const instanceId = await this.getInstanceId();
|
const instanceId = this.getInstanceId();
|
||||||
|
|
||||||
let responseData;
|
let responseData;
|
||||||
|
|
||||||
|
|
|
@ -271,7 +271,7 @@ export class MicrosoftTeams implements INodeType {
|
||||||
const resource = this.getNodeParameter('resource', 0);
|
const resource = this.getNodeParameter('resource', 0);
|
||||||
const operation = this.getNodeParameter('operation', 0);
|
const operation = this.getNodeParameter('operation', 0);
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
const instanceId = await this.getInstanceId();
|
const instanceId = this.getInstanceId();
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -207,7 +207,6 @@ describe('Test MySql V2, operations', () => {
|
||||||
|
|
||||||
fakeConnectionCopy.query = jest.fn(async (query?: string) => {
|
fakeConnectionCopy.query = jest.fn(async (query?: string) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
console.log(query);
|
|
||||||
if (query?.toLowerCase().includes('select')) {
|
if (query?.toLowerCase().includes('select')) {
|
||||||
result.push([{ id: 1, name: 'test 1' }]);
|
result.push([{ id: 1, name: 'test 1' }]);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -323,7 +323,7 @@ export class SlackV2 implements INodeType {
|
||||||
const operation = this.getNodeParameter('operation', 0);
|
const operation = this.getNodeParameter('operation', 0);
|
||||||
|
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
const instanceId = await this.getInstanceId();
|
const instanceId = this.getInstanceId();
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1708,7 +1708,7 @@ export class Telegram implements INodeType {
|
||||||
const binaryData = this.getNodeParameter('binaryData', 0, false);
|
const binaryData = this.getNodeParameter('binaryData', 0, false);
|
||||||
|
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
const nodeVersion = this.getNode().typeVersion;
|
||||||
const instanceId = await this.getInstanceId();
|
const instanceId = this.getInstanceId();
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -175,9 +175,8 @@ export function WorkflowExecuteAdditionalData(
|
||||||
credentialsHelper: new CredentialsHelper(credentialTypes),
|
credentialsHelper: new CredentialsHelper(credentialTypes),
|
||||||
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData),
|
||||||
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
|
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
|
||||||
sendMessageToUI: (message: string) => {},
|
sendDataToUI: (message: string) => {},
|
||||||
restApiUrl: '',
|
restApiUrl: '',
|
||||||
encryptionKey: 'test',
|
|
||||||
timezone: workflowTestData?.input.workflowData.settings?.timezone || 'America/New_York',
|
timezone: workflowTestData?.input.workflowData.settings?.timezone || 'America/New_York',
|
||||||
webhookBaseUrl: 'webhook',
|
webhookBaseUrl: 'webhook',
|
||||||
webhookWaitingBaseUrl: 'webhook-waiting',
|
webhookWaitingBaseUrl: 'webhook-waiting',
|
||||||
|
|
1
packages/nodes-base/test/setup.ts
Normal file
1
packages/nodes-base/test/setup.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import 'reflect-metadata';
|
|
@ -39,7 +39,7 @@
|
||||||
"dist/**/*"
|
"dist/**/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.3",
|
||||||
"@types/deep-equal": "^1.0.1",
|
"@types/deep-equal": "^1.0.1",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/jmespath": "^0.15.0",
|
"@types/jmespath": "^0.15.0",
|
||||||
|
|
|
@ -108,17 +108,13 @@ export abstract class ICredentials {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject;
|
abstract getData(nodeType?: string): ICredentialDataDecryptedObject;
|
||||||
|
|
||||||
abstract getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation;
|
|
||||||
|
|
||||||
abstract getDataToSave(): ICredentialsEncrypted;
|
abstract getDataToSave(): ICredentialsEncrypted;
|
||||||
|
|
||||||
abstract hasNodeAccess(nodeType: string): boolean;
|
abstract hasNodeAccess(nodeType: string): boolean;
|
||||||
|
|
||||||
abstract setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void;
|
abstract setData(data: ICredentialDataDecryptedObject): void;
|
||||||
|
|
||||||
abstract setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
|
@ -192,8 +188,6 @@ export interface IHttpRequestHelper {
|
||||||
helpers: { httpRequest: IAllExecuteFunctions['helpers']['httpRequest'] };
|
helpers: { httpRequest: IAllExecuteFunctions['helpers']['httpRequest'] };
|
||||||
}
|
}
|
||||||
export abstract class ICredentialsHelper {
|
export abstract class ICredentialsHelper {
|
||||||
constructor(readonly encryptionKey: string) {}
|
|
||||||
|
|
||||||
abstract getParentTypes(name: string): string[];
|
abstract getParentTypes(name: string): string[];
|
||||||
|
|
||||||
abstract authenticate(
|
abstract authenticate(
|
||||||
|
@ -740,7 +734,7 @@ export interface FunctionsBase {
|
||||||
getTimezone(): string;
|
getTimezone(): string;
|
||||||
getRestApiUrl(): string;
|
getRestApiUrl(): string;
|
||||||
getInstanceBaseUrl(): string;
|
getInstanceBaseUrl(): string;
|
||||||
getInstanceId(): Promise<string>;
|
getInstanceId(): string;
|
||||||
|
|
||||||
getMode?: () => WorkflowExecuteMode;
|
getMode?: () => WorkflowExecuteMode;
|
||||||
getActivationMode?: () => WorkflowActivateMode;
|
getActivationMode?: () => WorkflowActivateMode;
|
||||||
|
@ -1847,7 +1841,6 @@ export interface IWorkflowExecuteHooks {
|
||||||
|
|
||||||
export interface IWorkflowExecuteAdditionalData {
|
export interface IWorkflowExecuteAdditionalData {
|
||||||
credentialsHelper: ICredentialsHelper;
|
credentialsHelper: ICredentialsHelper;
|
||||||
encryptionKey: string;
|
|
||||||
executeWorkflow: (
|
executeWorkflow: (
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
@ -1860,7 +1853,6 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
parentWorkflowSettings?: IWorkflowSettings;
|
parentWorkflowSettings?: IWorkflowSettings;
|
||||||
},
|
},
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
// hooks?: IWorkflowExecuteHooks;
|
|
||||||
executionId?: string;
|
executionId?: string;
|
||||||
restartExecutionId?: string;
|
restartExecutionId?: string;
|
||||||
hooks?: WorkflowHooks;
|
hooks?: WorkflowHooks;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import type {
|
import type {
|
||||||
CredentialInformation,
|
|
||||||
IAdditionalCredentialOptions,
|
IAdditionalCredentialOptions,
|
||||||
IAllExecuteFunctions,
|
IAllExecuteFunctions,
|
||||||
IContextObject,
|
IContextObject,
|
||||||
|
@ -44,48 +43,21 @@ export interface INodeTypesObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Credentials extends ICredentials {
|
export class Credentials extends ICredentials {
|
||||||
hasNodeAccess(nodeType: string): boolean {
|
hasNodeAccess() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void {
|
setData(data: ICredentialDataDecryptedObject) {
|
||||||
this.data = JSON.stringify(data);
|
this.data = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void {
|
getData(): ICredentialDataDecryptedObject {
|
||||||
let fullData;
|
|
||||||
try {
|
|
||||||
fullData = this.getData(encryptionKey);
|
|
||||||
} catch (e) {
|
|
||||||
fullData = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
fullData[key] = data;
|
|
||||||
|
|
||||||
return this.setData(fullData, encryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject {
|
|
||||||
if (this.data === undefined) {
|
if (this.data === undefined) {
|
||||||
throw new Error('No data is set so nothing can be returned.');
|
throw new Error('No data is set so nothing can be returned.');
|
||||||
}
|
}
|
||||||
return JSON.parse(this.data);
|
return JSON.parse(this.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation {
|
|
||||||
const fullData = this.getData(encryptionKey, nodeType);
|
|
||||||
|
|
||||||
if (fullData === null) {
|
|
||||||
throw new Error('No data was set.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fullData.hasOwnProperty(key)) {
|
|
||||||
throw new Error(`No data for key "${key}" exists.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullData[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataToSave(): ICredentialsEncrypted {
|
getDataToSave(): ICredentialsEncrypted {
|
||||||
if (this.data === undefined) {
|
if (this.data === undefined) {
|
||||||
throw new Error('No credentials were set to save.');
|
throw new Error('No credentials were set to save.');
|
||||||
|
@ -702,12 +674,11 @@ export function WorkflowExecuteAdditionalData(): IWorkflowExecuteAdditionalData
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credentialsHelper: new CredentialsHelper(''),
|
credentialsHelper: new CredentialsHelper(),
|
||||||
hooks: new WorkflowHooks({}, 'trigger', '1', workflowData),
|
hooks: new WorkflowHooks({}, 'trigger', '1', workflowData),
|
||||||
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
|
executeWorkflow: async (workflowInfo: IExecuteWorkflowInfo): Promise<any> => {},
|
||||||
sendMessageToUI: (message: string) => {},
|
sendDataToUI: (message: string) => {},
|
||||||
restApiUrl: '',
|
restApiUrl: '',
|
||||||
encryptionKey: 'test',
|
|
||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
webhookBaseUrl: 'webhook',
|
webhookBaseUrl: 'webhook',
|
||||||
webhookWaitingBaseUrl: 'webhook-waiting',
|
webhookWaitingBaseUrl: 'webhook-waiting',
|
||||||
|
|
|
@ -260,9 +260,6 @@ importers:
|
||||||
cookie-parser:
|
cookie-parser:
|
||||||
specifier: ^1.4.6
|
specifier: ^1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
crypto-js:
|
|
||||||
specifier: ~4.1.1
|
|
||||||
version: 4.1.1
|
|
||||||
csrf:
|
csrf:
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
|
@ -586,7 +583,7 @@ importers:
|
||||||
specifier: ~1.7.2
|
specifier: ~1.7.2
|
||||||
version: 1.7.2
|
version: 1.7.2
|
||||||
crypto-js:
|
crypto-js:
|
||||||
specifier: ~4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
fast-glob:
|
fast-glob:
|
||||||
specifier: ^3.2.5
|
specifier: ^3.2.5
|
||||||
|
@ -644,8 +641,8 @@ importers:
|
||||||
specifier: ~1.7.1
|
specifier: ~1.7.1
|
||||||
version: 1.7.3
|
version: 1.7.3
|
||||||
'@types/crypto-js':
|
'@types/crypto-js':
|
||||||
specifier: ^4.0.1
|
specifier: ^4.1.3
|
||||||
version: 4.1.1
|
version: 4.1.3
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^4.17.6
|
specifier: ^4.17.6
|
||||||
version: 4.17.14
|
version: 4.17.14
|
||||||
|
@ -1008,6 +1005,9 @@ importers:
|
||||||
tmp-promise:
|
tmp-promise:
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
typedi:
|
||||||
|
specifier: ^0.10.0
|
||||||
|
version: 0.10.0(patch_hash=62r6bc2crgimafeyruodhqlgo4)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@oclif/dev-cli':
|
'@oclif/dev-cli':
|
||||||
specifier: ^1.22.2
|
specifier: ^1.22.2
|
||||||
|
@ -1348,8 +1348,8 @@ importers:
|
||||||
version: 0.5.0
|
version: 0.5.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/crypto-js':
|
'@types/crypto-js':
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.3
|
||||||
version: 4.1.1
|
version: 4.1.3
|
||||||
'@types/deep-equal':
|
'@types/deep-equal':
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
@ -6819,7 +6819,7 @@ packages:
|
||||||
ts-dedent: 2.2.0
|
ts-dedent: 2.2.0
|
||||||
type-fest: 3.13.1
|
type-fest: 3.13.1
|
||||||
vue: 3.3.4
|
vue: 3.3.4
|
||||||
vue-component-type-helpers: 1.8.15
|
vue-component-type-helpers: 1.8.19
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -7054,8 +7054,8 @@ packages:
|
||||||
'@types/node': 18.16.16
|
'@types/node': 18.16.16
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/crypto-js@4.1.1:
|
/@types/crypto-js@4.1.3:
|
||||||
resolution: {integrity: sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==}
|
resolution: {integrity: sha512-YP1sYYayLe7Eg5oXyLLvOLfxBfZ5Fgpz6sVWkpB18wDMywCLPWmqzRz+9gyuOoLF0fzDTTFwlyNbx7koONUwqA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/dateformat@3.0.1:
|
/@types/dateformat@3.0.1:
|
||||||
|
@ -21794,8 +21794,8 @@ packages:
|
||||||
vue: 3.3.4
|
vue: 3.3.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/vue-component-type-helpers@1.8.15:
|
/vue-component-type-helpers@1.8.19:
|
||||||
resolution: {integrity: sha512-RKiPRKW4BdwgmQ9vaNkHYKAThdTbgU4TOphVyyzqxRwsOJOoRIrb+vB49XLvs5CKPNrvxMXZMwPe5FyJCqFWyg==}
|
resolution: {integrity: sha512-1OANGSZK4pzHF4uc86usWi+o5Y0zgoDtqWkPg6Am6ot+jHSAmpOah59V/4N82So5xRgivgCxGgK09lBy1XNUfQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vue-component-type-helpers@1.8.4:
|
/vue-component-type-helpers@1.8.4:
|
||||||
|
@ -22555,7 +22555,7 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
'@cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz':
|
'@cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz':
|
||||||
resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz}
|
resolution: {registry: https://registry.npmjs.org/, tarball: https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz}
|
||||||
name: xlsx
|
name: xlsx
|
||||||
version: 0.19.3
|
version: 0.19.3
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
Loading…
Reference in a new issue