mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(core): Simplify ExternalSecretsProxy setup and move it to core (#16021)
This commit is contained in:
committed by
GitHub
parent
3e91f3253b
commit
2258a74518
@@ -4,6 +4,7 @@ import { ExecutionRepository } from '@n8n/db';
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { ExternalSecretsProxy } from 'n8n-core';
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteWorkflowInfo,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
SubworkflowPolicyChecker,
|
||||
} from '@/executions/pre-execution-checks';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import { SecretsHelper } from '@/secrets-helpers.ee';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
@@ -86,12 +86,12 @@ describe('WorkflowExecuteAdditionalData', () => {
|
||||
const variablesService = mockInstance(VariablesService);
|
||||
variablesService.getAllCached.mockResolvedValue([]);
|
||||
const credentialsHelper = mockInstance(CredentialsHelper);
|
||||
const secretsHelper = mockInstance(SecretsHelper);
|
||||
const externalSecretsProxy = mockInstance(ExternalSecretsProxy);
|
||||
const eventService = mockInstance(EventService);
|
||||
mockInstance(ExternalHooks);
|
||||
Container.set(VariablesService, variablesService);
|
||||
Container.set(CredentialsHelper, credentialsHelper);
|
||||
Container.set(SecretsHelper, secretsHelper);
|
||||
Container.set(ExternalSecretsProxy, externalSecretsProxy);
|
||||
const executionRepository = mockInstance(ExecutionRepository);
|
||||
mockInstance(Telemetry);
|
||||
const workflowRepository = mockInstance(WorkflowRepository);
|
||||
@@ -306,7 +306,7 @@ describe('WorkflowExecuteAdditionalData', () => {
|
||||
userId: undefined,
|
||||
setExecutionStatus: expect.any(Function),
|
||||
variables: mockVariables,
|
||||
secretsHelpers: secretsHelper,
|
||||
externalSecretsProxy,
|
||||
startRunnerTask: expect.any(Function),
|
||||
logAiEvent: expect.any(Function),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ObjectStoreService,
|
||||
DataDeduplicationService,
|
||||
ErrorReporter,
|
||||
ExternalSecretsProxy,
|
||||
} from 'n8n-core';
|
||||
import { ensureError, sleep, UserError } from 'n8n-workflow';
|
||||
|
||||
@@ -278,6 +279,7 @@ export abstract class BaseCommand extends Command {
|
||||
async initExternalSecrets() {
|
||||
const secretsManager = Container.get(ExternalSecretsManager);
|
||||
await secretsManager.init();
|
||||
Container.get(ExternalSecretsProxy).setManager(secretsManager);
|
||||
}
|
||||
|
||||
initWorkflowHistory() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Container } from '@n8n/di';
|
||||
import Csrf from 'csrf';
|
||||
import type { Response } from 'express';
|
||||
import { captor, mock } from 'jest-mock-extended';
|
||||
import { Cipher, type InstanceSettings } from 'n8n-core';
|
||||
import { Cipher, type InstanceSettings, ExternalSecretsProxy } from 'n8n-core';
|
||||
import type { IWorkflowExecuteAdditionalData } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
@@ -19,7 +19,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import type { OAuthRequest } from '@/requests';
|
||||
import { SecretsHelper } from '@/secrets-helpers.ee';
|
||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
@@ -28,7 +27,7 @@ jest.mock('@/workflow-execute-additional-data');
|
||||
describe('OAuth1CredentialController', () => {
|
||||
mockInstance(Logger);
|
||||
mockInstance(ExternalHooks);
|
||||
mockInstance(SecretsHelper);
|
||||
mockInstance(ExternalSecretsProxy);
|
||||
mockInstance(VariablesService, {
|
||||
getAllCached: async () => [],
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Container } from '@n8n/di';
|
||||
import Csrf from 'csrf';
|
||||
import { type Response } from 'express';
|
||||
import { captor, mock } from 'jest-mock-extended';
|
||||
import { Cipher, type InstanceSettings } from 'n8n-core';
|
||||
import { Cipher, type InstanceSettings, ExternalSecretsProxy } from 'n8n-core';
|
||||
import type { IWorkflowExecuteAdditionalData } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
@@ -19,7 +19,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import type { OAuthRequest } from '@/requests';
|
||||
import { SecretsHelper } from '@/secrets-helpers.ee';
|
||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
@@ -27,7 +26,7 @@ jest.mock('@/workflow-execute-additional-data');
|
||||
|
||||
describe('OAuth2CredentialController', () => {
|
||||
mockInstance(Logger);
|
||||
mockInstance(SecretsHelper);
|
||||
mockInstance(ExternalSecretsProxy);
|
||||
mockInstance(VariablesService, {
|
||||
getAllCached: async () => [],
|
||||
});
|
||||
|
||||
@@ -327,8 +327,6 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||
return decryptedDataOriginal;
|
||||
}
|
||||
|
||||
await additionalData?.secretsHelpers?.waitForInit();
|
||||
|
||||
return await this.applyDefaultsAndOverwrites(
|
||||
additionalData,
|
||||
decryptedDataOriginal,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Container } from '@n8n/di';
|
||||
import axios from 'axios';
|
||||
import type { AxiosRequestConfig, Method } from 'axios';
|
||||
import { Agent as HTTPSAgent } from 'https';
|
||||
import { ExternalSecretsProxy } from 'n8n-core';
|
||||
import { jsonParse, MessageEventBusDestinationTypeNames } from 'n8n-workflow';
|
||||
import type {
|
||||
MessageEventBusDestinationOptions,
|
||||
@@ -14,7 +15,6 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { CredentialsHelper } from '@/credentials-helper';
|
||||
import { SecretsHelper } from '@/secrets-helpers.ee';
|
||||
|
||||
import { MessageEventBusDestination } from './message-event-bus-destination.ee';
|
||||
import { eventMessageGenericDestinationTestEvent } from '../event-message-classes/event-message-generic';
|
||||
@@ -103,7 +103,7 @@ export class MessageEventBusDestinationWebhook
|
||||
if (foundCredential) {
|
||||
const credentialsDecrypted = await this.credentialsHelper?.getDecrypted(
|
||||
{
|
||||
secretsHelpers: Container.get(SecretsHelper),
|
||||
externalSecretsProxy: Container.get(ExternalSecretsProxy),
|
||||
} as unknown as IWorkflowExecuteAdditionalData,
|
||||
foundCredential[1],
|
||||
foundCredential[0],
|
||||
|
||||
@@ -352,10 +352,10 @@ describe('External Secrets Manager', () => {
|
||||
expect(manager.getSecretNames('dummy')).toEqual(['test1', 'test2']);
|
||||
});
|
||||
|
||||
test('should return undefined when provider does not exist', async () => {
|
||||
test('should return an empty array when provider does not exist', async () => {
|
||||
await manager.init();
|
||||
|
||||
expect(manager.getSecretNames('nonexistent')).toBeUndefined();
|
||||
expect(manager.getSecretNames('nonexistent')).toBeEmptyArray();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Logger } from '@n8n/backend-common';
|
||||
import { SettingsRepository } from '@n8n/db';
|
||||
import { OnPubSubEvent, OnShutdown } from '@n8n/decorators';
|
||||
import { Service } from '@n8n/di';
|
||||
import { Cipher } from 'n8n-core';
|
||||
import { Cipher, type IExternalSecretsManager } from 'n8n-core';
|
||||
import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
import { EventService } from '@/events/event.service';
|
||||
@@ -19,7 +19,7 @@ import { ExternalSecretsConfig } from './external-secrets.config';
|
||||
import type { ExternalSecretsSettings, SecretsProvider, SecretsProviderSettings } from './types';
|
||||
|
||||
@Service()
|
||||
export class ExternalSecretsManager {
|
||||
export class ExternalSecretsManager implements IExternalSecretsManager {
|
||||
private providers: Record<string, SecretsProvider> = {};
|
||||
|
||||
private initializingPromise?: Promise<void>;
|
||||
@@ -211,7 +211,7 @@ export class ExternalSecretsManager {
|
||||
return provider in this.providers;
|
||||
}
|
||||
|
||||
getProviderNames(): string[] | undefined {
|
||||
getProviderNames(): string[] {
|
||||
return Object.keys(this.providers);
|
||||
}
|
||||
|
||||
@@ -223,8 +223,8 @@ export class ExternalSecretsManager {
|
||||
return this.getProvider(provider)?.hasSecret(name) ?? false;
|
||||
}
|
||||
|
||||
getSecretNames(provider: string): string[] | undefined {
|
||||
return this.getProvider(provider)?.getSecretNames();
|
||||
getSecretNames(provider: string): string[] {
|
||||
return this.getProvider(provider)?.getSecretNames() ?? [];
|
||||
}
|
||||
|
||||
getAllSecretNames(): Record<string, string[]> {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IExecutionResponse } from '@n8n/db';
|
||||
import type { ExecutionRepository } from '@n8n/db';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { WorkflowExecute as ActualWorkflowExecute } from 'n8n-core';
|
||||
import { ExternalSecretsProxy } from 'n8n-core';
|
||||
import { mockInstance } from 'n8n-core/test/utils';
|
||||
import type { IPinData, ITaskData, IWorkflowExecuteAdditionalData } from 'n8n-workflow';
|
||||
import { Workflow, type IRunExecutionData, type WorkflowExecuteMode } from 'n8n-workflow';
|
||||
@@ -11,7 +12,6 @@ import { CredentialsHelper } from '@/credentials-helper';
|
||||
import { VariablesService } from '@/environments.ee/variables/variables.service.ee';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import type { ManualExecutionService } from '@/manual-execution.service';
|
||||
import { SecretsHelper } from '@/secrets-helpers.ee';
|
||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||
import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
|
||||
@@ -23,7 +23,7 @@ mockInstance(VariablesService, {
|
||||
getAllCached: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
mockInstance(CredentialsHelper);
|
||||
mockInstance(SecretsHelper);
|
||||
mockInstance(ExternalSecretsProxy);
|
||||
mockInstance(WorkflowStaticDataService);
|
||||
mockInstance(WorkflowStatisticsService);
|
||||
mockInstance(ExternalHooks);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Service } from '@n8n/di';
|
||||
import type { SecretsHelpersBase } from 'n8n-workflow';
|
||||
|
||||
import { ExternalSecretsManager } from './external-secrets.ee/external-secrets-manager.ee';
|
||||
|
||||
@Service()
|
||||
export class SecretsHelper implements SecretsHelpersBase {
|
||||
constructor(private service: ExternalSecretsManager) {}
|
||||
|
||||
async update() {
|
||||
if (!this.service.initialized) {
|
||||
await this.service.init();
|
||||
}
|
||||
await this.service.updateSecrets();
|
||||
}
|
||||
|
||||
async waitForInit() {
|
||||
if (!this.service.initialized) {
|
||||
await this.service.init();
|
||||
}
|
||||
}
|
||||
|
||||
getSecret(provider: string, name: string) {
|
||||
return this.service.getSecret(provider, name);
|
||||
}
|
||||
|
||||
hasSecret(provider: string, name: string): boolean {
|
||||
return this.service.hasSecret(provider, name);
|
||||
}
|
||||
|
||||
hasProvider(provider: string): boolean {
|
||||
return this.service.hasProvider(provider);
|
||||
}
|
||||
|
||||
listProviders(): string[] {
|
||||
return this.service.getProviderNames() ?? [];
|
||||
}
|
||||
|
||||
listSecrets(provider: string): string[] {
|
||||
return this.service.getSecretNames(provider) ?? [];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { Logger } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { ExecutionRepository, WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { WorkflowExecute } from 'n8n-core';
|
||||
import { ExternalSecretsProxy, WorkflowExecute } from 'n8n-core';
|
||||
import { UnexpectedError, Workflow } from 'n8n-workflow';
|
||||
import type {
|
||||
IDataObject,
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
import type { UpdateExecutionPayload } from '@/interfaces';
|
||||
import { NodeTypes } from '@/node-types';
|
||||
import { Push } from '@/push';
|
||||
import { SecretsHelper } from '@/secrets-helpers.ee';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { TaskRequester } from '@/task-runners/task-managers/task-requester';
|
||||
import { findSubworkflowStart } from '@/utils';
|
||||
@@ -388,7 +387,7 @@ export async function getBase(
|
||||
userId,
|
||||
setExecutionStatus,
|
||||
variables,
|
||||
secretsHelpers: Container.get(SecretsHelper),
|
||||
externalSecretsProxy: Container.get(ExternalSecretsProxy),
|
||||
async startRunnerTask(
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
jobType: string,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { WebhookEntity } from '@n8n/db';
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import { InstanceSettings, ExternalSecretsProxy } from 'n8n-core';
|
||||
import { FormTrigger } from 'n8n-nodes-base/nodes/Form/FormTrigger.node';
|
||||
import { ScheduleTrigger } from 'n8n-nodes-base/nodes/Schedule/ScheduleTrigger.node';
|
||||
import { NodeApiError, Workflow } from 'n8n-workflow';
|
||||
@@ -19,7 +19,6 @@ import { ExecutionService } from '@/executions/execution.service';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import { NodeTypes } from '@/node-types';
|
||||
import { Push } from '@/push';
|
||||
import { SecretsHelper } from '@/secrets-helpers.ee';
|
||||
import * as WebhookHelpers from '@/webhooks/webhook-helpers';
|
||||
import { WebhookService } from '@/webhooks/webhook.service';
|
||||
import * as AdditionalData from '@/workflow-execute-additional-data';
|
||||
@@ -33,7 +32,7 @@ import { mockInstance } from '../shared/mocking';
|
||||
|
||||
mockInstance(ActiveExecutions);
|
||||
mockInstance(Push);
|
||||
mockInstance(SecretsHelper);
|
||||
mockInstance(ExternalSecretsProxy);
|
||||
mockInstance(ExecutionService);
|
||||
mockInstance(WorkflowService);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
|
||||
import type {
|
||||
ExecutionLifecycleHookName,
|
||||
ExecutionLifecyleHookHandlers,
|
||||
ExecutionLifecycleHookHandlers,
|
||||
} from '../execution-lifecycle-hooks';
|
||||
import { ExecutionLifecycleHooks } from '../execution-lifecycle-hooks';
|
||||
|
||||
@@ -46,12 +46,14 @@ describe('ExecutionLifecycleHooks', () => {
|
||||
describe('addHandler()', () => {
|
||||
const hooksHandlers =
|
||||
mock<{
|
||||
[K in keyof ExecutionLifecyleHookHandlers]: ExecutionLifecyleHookHandlers[K][number];
|
||||
[K in keyof ExecutionLifecycleHookHandlers]: ExecutionLifecycleHookHandlers[K][number];
|
||||
}>();
|
||||
|
||||
const testCases: Array<{
|
||||
hook: ExecutionLifecycleHookName;
|
||||
args: Parameters<ExecutionLifecyleHookHandlers[keyof ExecutionLifecyleHookHandlers][number]>;
|
||||
args: Parameters<
|
||||
ExecutionLifecycleHookHandlers[keyof ExecutionLifecycleHookHandlers][number]
|
||||
>;
|
||||
}> = [
|
||||
{ hook: 'nodeExecuteBefore', args: ['testNode', mock<ITaskStartedData>()] },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import { ExternalSecretsProxy, type IExternalSecretsManager } from '../external-secrets-proxy';
|
||||
|
||||
describe('ExternalSecretsProxy', () => {
|
||||
let proxy: ExternalSecretsProxy;
|
||||
const manager = mock<IExternalSecretsManager>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
proxy = new ExternalSecretsProxy();
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update secrets when manager is set', async () => {
|
||||
manager.updateSecrets.mockResolvedValue();
|
||||
proxy.setManager(manager);
|
||||
|
||||
await proxy.update();
|
||||
|
||||
expect(manager.updateSecrets).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not throw when updating without a manager', async () => {
|
||||
await expect(proxy.update()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSecret', () => {
|
||||
it('should get secret from manager', () => {
|
||||
const secretValue = { key: 'value' };
|
||||
manager.getSecret.mockReturnValue(secretValue);
|
||||
proxy.setManager(manager);
|
||||
|
||||
const result = proxy.getSecret('aws', 'api-key');
|
||||
|
||||
expect(manager.getSecret).toHaveBeenCalledWith('aws', 'api-key');
|
||||
expect(result).toBe(secretValue);
|
||||
});
|
||||
|
||||
it('should return undefined when getting secret without a manager', () => {
|
||||
const result = proxy.getSecret('aws', 'api-key');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSecret', () => {
|
||||
it('should check if secret exists', () => {
|
||||
manager.hasSecret.mockReturnValue(true);
|
||||
proxy.setManager(manager);
|
||||
|
||||
const result = proxy.hasSecret('aws', 'api-key');
|
||||
|
||||
expect(manager.hasSecret).toHaveBeenCalledWith('aws', 'api-key');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when checking secret without a manager', () => {
|
||||
const result = proxy.hasSecret('aws', 'api-key');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasProvider', () => {
|
||||
it('should check if provider exists', () => {
|
||||
manager.hasProvider.mockReturnValue(true);
|
||||
proxy.setManager(manager);
|
||||
|
||||
const result = proxy.hasProvider('aws');
|
||||
|
||||
expect(manager.hasProvider).toHaveBeenCalledWith('aws');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when checking provider without a manager', () => {
|
||||
const result = proxy.hasProvider('aws');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listProviders', () => {
|
||||
it('should list providers', () => {
|
||||
const providers = ['aws', 'gcp', 'azure'];
|
||||
manager.getProviderNames.mockReturnValue(providers);
|
||||
proxy.setManager(manager);
|
||||
|
||||
const result = proxy.listProviders();
|
||||
|
||||
expect(manager.getProviderNames).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(providers);
|
||||
});
|
||||
|
||||
it('should return empty array when listing providers without a manager', () => {
|
||||
const result = proxy.listProviders();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listSecrets', () => {
|
||||
it('should list secrets for a provider', () => {
|
||||
const secrets = ['api-key', 'api-secret', 'token'];
|
||||
manager.getSecretNames.mockReturnValue(secrets);
|
||||
proxy.setManager(manager);
|
||||
|
||||
const result = proxy.listSecrets('aws');
|
||||
|
||||
expect(manager.getSecretNames).toHaveBeenCalledWith('aws');
|
||||
expect(result).toEqual(secrets);
|
||||
});
|
||||
|
||||
it('should return empty array when listing secrets without a manager', () => {
|
||||
const result = proxy.listSecrets('aws');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export type ExecutionLifecyleHookHandlers = {
|
||||
export type ExecutionLifecycleHookHandlers = {
|
||||
nodeExecuteBefore: Array<
|
||||
(
|
||||
this: ExecutionLifecycleHooks,
|
||||
@@ -56,7 +56,7 @@ s */
|
||||
>;
|
||||
};
|
||||
|
||||
export type ExecutionLifecycleHookName = keyof ExecutionLifecyleHookHandlers;
|
||||
export type ExecutionLifecycleHookName = keyof ExecutionLifecycleHookHandlers;
|
||||
|
||||
/**
|
||||
* Contains hooks that trigger at specific events in an execution's lifecycle. Every hook has an array of callbacks to run.
|
||||
@@ -77,7 +77,7 @@ export type ExecutionLifecycleHookName = keyof ExecutionLifecyleHookHandlers;
|
||||
* ```
|
||||
*/
|
||||
export class ExecutionLifecycleHooks {
|
||||
readonly handlers: ExecutionLifecyleHookHandlers = {
|
||||
readonly handlers: ExecutionLifecycleHookHandlers = {
|
||||
nodeExecuteAfter: [],
|
||||
nodeExecuteBefore: [],
|
||||
nodeFetchedData: [],
|
||||
@@ -92,18 +92,18 @@ export class ExecutionLifecycleHooks {
|
||||
readonly workflowData: IWorkflowBase,
|
||||
) {}
|
||||
|
||||
addHandler<Hook extends keyof ExecutionLifecyleHookHandlers>(
|
||||
addHandler<Hook extends keyof ExecutionLifecycleHookHandlers>(
|
||||
hookName: Hook,
|
||||
...handlers: Array<ExecutionLifecyleHookHandlers[Hook][number]>
|
||||
...handlers: Array<ExecutionLifecycleHookHandlers[Hook][number]>
|
||||
): void {
|
||||
// @ts-expect-error FIX THIS
|
||||
this.handlers[hookName].push(...handlers);
|
||||
}
|
||||
|
||||
async runHook<
|
||||
Hook extends keyof ExecutionLifecyleHookHandlers,
|
||||
Hook extends keyof ExecutionLifecycleHookHandlers,
|
||||
Params extends unknown[] = Parameters<
|
||||
Exclude<ExecutionLifecyleHookHandlers[Hook], undefined>[number]
|
||||
Exclude<ExecutionLifecycleHookHandlers[Hook], undefined>[number]
|
||||
>,
|
||||
>(hookName: Hook, parameters: Params) {
|
||||
const hooks = this.handlers[hookName];
|
||||
@@ -116,9 +116,3 @@ export class ExecutionLifecycleHooks {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'n8n-workflow' {
|
||||
interface IWorkflowExecuteAdditionalData {
|
||||
hooks?: ExecutionLifecycleHooks;
|
||||
}
|
||||
}
|
||||
|
||||
43
packages/core/src/execution-engine/external-secrets-proxy.ts
Normal file
43
packages/core/src/execution-engine/external-secrets-proxy.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Service } from '@n8n/di';
|
||||
|
||||
export interface IExternalSecretsManager {
|
||||
updateSecrets(): Promise<void>;
|
||||
hasSecret(provider: string, name: string): boolean;
|
||||
getSecret(provider: string, name: string): unknown;
|
||||
getSecretNames(provider: string): string[];
|
||||
hasProvider(provider: string): boolean;
|
||||
getProviderNames(): string[];
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class ExternalSecretsProxy {
|
||||
private manager?: IExternalSecretsManager;
|
||||
|
||||
setManager(manager: IExternalSecretsManager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
async update() {
|
||||
await this.manager?.updateSecrets();
|
||||
}
|
||||
|
||||
getSecret(provider: string, name: string) {
|
||||
return this.manager?.getSecret(provider, name);
|
||||
}
|
||||
|
||||
hasSecret(provider: string, name: string): boolean {
|
||||
return !!this.manager && this.manager.hasSecret(provider, name);
|
||||
}
|
||||
|
||||
hasProvider(provider: string): boolean {
|
||||
return !!this.manager && this.manager.hasProvider(provider);
|
||||
}
|
||||
|
||||
listProviders(): string[] {
|
||||
return this.manager?.getProviderNames() ?? [];
|
||||
}
|
||||
|
||||
listSecrets(provider: string): string[] {
|
||||
return this.manager?.getSecretNames(provider) ?? [];
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
import type { ExecutionLifecycleHooks } from './execution-lifecycle-hooks';
|
||||
import type { ExternalSecretsProxy } from './external-secrets-proxy';
|
||||
|
||||
declare module 'n8n-workflow' {
|
||||
interface IWorkflowExecuteAdditionalData {
|
||||
hooks?: ExecutionLifecycleHooks;
|
||||
externalSecretsProxy: ExternalSecretsProxy;
|
||||
}
|
||||
}
|
||||
|
||||
export * from './active-workflows';
|
||||
export * from './interfaces';
|
||||
export * from './routing-node';
|
||||
@@ -6,3 +16,4 @@ export * from './partial-execution-utils';
|
||||
export * from './node-execution-context/utils/execution-metadata';
|
||||
export * from './workflow-execute';
|
||||
export { ExecutionLifecycleHooks } from './execution-lifecycle-hooks';
|
||||
export { ExternalSecretsProxy, type IExternalSecretsManager } from './external-secrets-proxy';
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
import type {
|
||||
IDataObject,
|
||||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
SecretsHelpersBase,
|
||||
} from 'n8n-workflow';
|
||||
import type { IDataObject, IRunExecutionData, IWorkflowExecuteAdditionalData } from 'n8n-workflow';
|
||||
|
||||
import { PLACEHOLDER_EMPTY_EXECUTION_ID } from '@/constants';
|
||||
import type { ExternalSecretsProxy } from '@/execution-engine/external-secrets-proxy';
|
||||
|
||||
import { getAdditionalKeys } from '../get-additional-keys';
|
||||
|
||||
describe('getAdditionalKeys', () => {
|
||||
const secretsHelpers = mock<SecretsHelpersBase>();
|
||||
const externalSecretsProxy = mock<ExternalSecretsProxy>();
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||
executionId: '123',
|
||||
webhookWaitingBaseUrl: 'https://webhook.test',
|
||||
formWaitingBaseUrl: 'https://form.test',
|
||||
variables: { testVar: 'value' },
|
||||
secretsHelpers,
|
||||
externalSecretsProxy,
|
||||
});
|
||||
|
||||
const runExecutionData = mock<IRunExecutionData>({
|
||||
@@ -30,11 +26,11 @@ describe('getAdditionalKeys', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
LoggerProxy.init(mock());
|
||||
secretsHelpers.hasProvider.mockReturnValue(true);
|
||||
secretsHelpers.hasSecret.mockReturnValue(true);
|
||||
secretsHelpers.getSecret.mockReturnValue('secret-value');
|
||||
secretsHelpers.listSecrets.mockReturnValue(['secret1']);
|
||||
secretsHelpers.listProviders.mockReturnValue(['provider1']);
|
||||
externalSecretsProxy.hasProvider.mockReturnValue(true);
|
||||
externalSecretsProxy.hasSecret.mockReturnValue(true);
|
||||
externalSecretsProxy.getSecret.mockReturnValue('secret-value');
|
||||
externalSecretsProxy.listSecrets.mockReturnValue(['secret1']);
|
||||
externalSecretsProxy.listProviders.mockReturnValue(['provider1']);
|
||||
});
|
||||
|
||||
it('should use placeholder execution ID when none provided', () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ function buildSecretsValueProxy(value: IDataObject): unknown {
|
||||
}
|
||||
|
||||
export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData): IDataObject {
|
||||
const secretsHelpers = additionalData.secretsHelpers;
|
||||
const { externalSecretsProxy } = additionalData;
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
@@ -31,7 +31,7 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData):
|
||||
if (typeof providerName !== 'string') {
|
||||
return {};
|
||||
}
|
||||
if (secretsHelpers.hasProvider(providerName)) {
|
||||
if (externalSecretsProxy.hasProvider(providerName)) {
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
@@ -39,13 +39,13 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData):
|
||||
if (typeof secretName !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (!secretsHelpers.hasSecret(providerName, secretName)) {
|
||||
if (!externalSecretsProxy.hasSecret(providerName, secretName)) {
|
||||
throw new ExpressionError('Could not load secrets', {
|
||||
description:
|
||||
'The credential in use tries to use secret from an external store that could not be found',
|
||||
});
|
||||
}
|
||||
const retValue = secretsHelpers.getSecret(providerName, secretName);
|
||||
const retValue = externalSecretsProxy.getSecret(providerName, secretName);
|
||||
if (typeof retValue === 'object' && retValue !== null) {
|
||||
return buildSecretsValueProxy(retValue as IDataObject);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData):
|
||||
return false;
|
||||
},
|
||||
ownKeys() {
|
||||
return secretsHelpers.listSecrets(providerName);
|
||||
return externalSecretsProxy.listSecrets(providerName);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -69,7 +69,7 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData):
|
||||
return false;
|
||||
},
|
||||
ownKeys() {
|
||||
return secretsHelpers.listProviders();
|
||||
return externalSecretsProxy.listProviders();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2374,7 +2374,6 @@ export interface IWorkflowExecuteAdditionalData {
|
||||
executionTimeoutTimestamp?: number;
|
||||
userId?: string;
|
||||
variables: IDataObject;
|
||||
secretsHelpers: SecretsHelpersBase;
|
||||
logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => void;
|
||||
parentCallbackManager?: CallbackManager;
|
||||
startRunnerTask<T, E = unknown>(
|
||||
@@ -2870,17 +2869,6 @@ export interface ICheckProcessedContextData {
|
||||
|
||||
export type N8nAIProviderType = 'openai' | 'unknown';
|
||||
|
||||
export interface SecretsHelpersBase {
|
||||
update(): Promise<void>;
|
||||
waitForInit(): Promise<void>;
|
||||
|
||||
getSecret(provider: string, name: string): unknown;
|
||||
hasSecret(provider: string, name: string): boolean;
|
||||
hasProvider(provider: string): boolean;
|
||||
listProviders(): string[];
|
||||
listSecrets(provider: string): string[];
|
||||
}
|
||||
|
||||
export type Functionality = 'regular' | 'configuration-node' | 'pairedItem';
|
||||
|
||||
export type CallbackManager = CallbackManagerLC;
|
||||
|
||||
Reference in New Issue
Block a user