mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
feat(core): Wire credentialDecrypt host point (no-changelog) (#36962)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import type {
|
||||
IAuthenticateGeneric,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialType,
|
||||
IExecuteData,
|
||||
IHttpRequestHelper,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
@@ -50,6 +51,7 @@ import type { CredentialsOverwrites } from '@/credentials-overwrites';
|
||||
import { CredentialNotFoundError } from '@/errors/credential-not-found.error';
|
||||
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import type { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config';
|
||||
import type { PolicyEnforcementService } from '@/policy/policy-enforcement.service';
|
||||
import type { AiGatewayService } from '@/services/ai-gateway.service';
|
||||
|
||||
describe('CredentialsHelper', () => {
|
||||
@@ -60,6 +62,7 @@ describe('CredentialsHelper', () => {
|
||||
const licenseState = mock<LicenseState>();
|
||||
const externalSecretsConfig = mock<ExternalSecretsConfig>();
|
||||
const mockLogger = mock<any>();
|
||||
const policyEnforcementService = mock<PolicyEnforcementService>();
|
||||
// Use a real instance of DynamicCredentialsProxy so setResolverProvider works
|
||||
const dynamicCredentialProxy = new DynamicCredentialsProxy(mockLogger);
|
||||
|
||||
@@ -81,6 +84,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
describe('getCredentials', () => {
|
||||
@@ -179,6 +183,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
const result = await helper.applyDefaultsAndOverwrites(
|
||||
@@ -232,6 +237,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -275,6 +281,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
const externalSecretsProxy =
|
||||
mock<NonNullable<IWorkflowExecuteAdditionalData['externalSecretsProxy']>>();
|
||||
@@ -354,6 +361,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
// Region left at its default: neither `region` nor `url` was persisted.
|
||||
@@ -413,6 +421,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
const result = await helper.applyDefaultsAndOverwrites(
|
||||
@@ -455,6 +464,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
const registerType = (credentialType: ICredentialType) =>
|
||||
@@ -1243,6 +1253,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
aiGatewayService,
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
const syntheticCred = { apiKey: 'mock-jwt', host: 'http://gateway/v1/gateway/google' };
|
||||
@@ -1288,6 +1299,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
aiGatewayService,
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
const syntheticCred = { apiKey: 'mock-jwt', host: 'http://gateway/v1/gateway/google' };
|
||||
@@ -1335,6 +1347,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
aiGatewayService,
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
const syntheticCred = {
|
||||
@@ -1570,6 +1583,7 @@ describe('CredentialsHelper', () => {
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
const result = await helperWithoutProvider.getDecrypted(
|
||||
@@ -1830,6 +1844,7 @@ describe('CredentialsHelper', () => {
|
||||
mock<LicenseState>(),
|
||||
mock<ExternalSecretsConfig>(),
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
|
||||
// The loader sets the class's `supportedNodes` to short names (e.g. "restrictedConsumer");
|
||||
@@ -2384,4 +2399,138 @@ describe('CredentialsHelper', () => {
|
||||
expect(result).toMatchObject({ accessToken: 'NEW_TOKEN' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDecrypted - credentialDecrypt policy enforcement', () => {
|
||||
const nodeCredentials: INodeCredentialsDetails = {
|
||||
id: 'cred-policy',
|
||||
name: 'Policy Test Credential',
|
||||
};
|
||||
|
||||
const credentialEntity = {
|
||||
id: 'cred-policy',
|
||||
name: 'Policy Test Credential',
|
||||
type: 'testApi',
|
||||
data: cipher.encrypt({ apiKey: 'test' }),
|
||||
isResolvable: false,
|
||||
usageScope: 'project',
|
||||
} as CredentialsEntity;
|
||||
|
||||
let helper: CredentialsHelper;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
credentialsRepository.findOneByOrFail.mockResolvedValue(credentialEntity);
|
||||
policyEnforcementService.enforceCredentialDecrypt.mockResolvedValue(mock());
|
||||
helper = new CredentialsHelper(
|
||||
new CredentialTypes(mockNodesAndCredentials),
|
||||
mock(),
|
||||
credentialsRepository,
|
||||
dynamicCredentialProxy,
|
||||
secretsProviderRepository,
|
||||
licenseState,
|
||||
externalSecretsConfig,
|
||||
mock<AiGatewayService>(),
|
||||
policyEnforcementService,
|
||||
);
|
||||
});
|
||||
|
||||
test('calls enforceCredentialDecrypt with the credential, consumer and project context', async () => {
|
||||
const executeData = {
|
||||
node: {
|
||||
name: 'Slack1',
|
||||
type: 'n8n-nodes-base.slack',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
data: {},
|
||||
source: null,
|
||||
} as IExecuteData;
|
||||
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({ projectId: 'proj-1' });
|
||||
|
||||
await helper.getDecrypted(
|
||||
additionalData,
|
||||
nodeCredentials,
|
||||
'testApi',
|
||||
'manual',
|
||||
executeData,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(policyEnforcementService.enforceCredentialDecrypt).toHaveBeenCalledExactlyOnceWith({
|
||||
credentialType: 'testApi',
|
||||
credentialId: 'cred-policy',
|
||||
consumer: { nodeType: 'n8n-nodes-base.slack' },
|
||||
projectId: 'proj-1',
|
||||
});
|
||||
});
|
||||
|
||||
test('passes a null consumer when no node is asking, e.g. a credential test', async () => {
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>({ projectId: undefined });
|
||||
|
||||
await helper.getDecrypted(
|
||||
additionalData,
|
||||
nodeCredentials,
|
||||
'testApi',
|
||||
'manual',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(policyEnforcementService.enforceCredentialDecrypt).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ consumer: null, projectId: null }),
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves the credential before enforcing the policy check', async () => {
|
||||
const callOrder: string[] = [];
|
||||
credentialsRepository.findOneByOrFail.mockImplementation(async () => {
|
||||
callOrder.push('findOneByOrFail');
|
||||
return credentialEntity;
|
||||
});
|
||||
policyEnforcementService.enforceCredentialDecrypt.mockImplementation(async () => {
|
||||
callOrder.push('enforceCredentialDecrypt');
|
||||
return await mock();
|
||||
});
|
||||
|
||||
await helper.getDecrypted(
|
||||
mock<IWorkflowExecuteAdditionalData>(),
|
||||
nodeCredentials,
|
||||
'testApi',
|
||||
'manual',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(callOrder).toEqual(['findOneByOrFail', 'enforceCredentialDecrypt']);
|
||||
});
|
||||
|
||||
test('blocks decryption when the policy check throws', async () => {
|
||||
const violation = new Error('blocked by policy');
|
||||
policyEnforcementService.enforceCredentialDecrypt.mockRejectedValueOnce(violation);
|
||||
|
||||
await expect(
|
||||
helper.getDecrypted(
|
||||
mock<IWorkflowExecuteAdditionalData>(),
|
||||
nodeCredentials,
|
||||
'testApi',
|
||||
'manual',
|
||||
),
|
||||
).rejects.toThrow(violation);
|
||||
});
|
||||
|
||||
test('decryption behavior is unchanged when the policy check clears', async () => {
|
||||
const result = await helper.getDecrypted(
|
||||
mock<IWorkflowExecuteAdditionalData>(),
|
||||
nodeCredentials,
|
||||
'testApi',
|
||||
'manual',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ apiKey: 'test' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,7 +271,9 @@ describe('WorkflowExecuteAdditionalData', () => {
|
||||
mock<ExecuteWorkflowOptions>({ loadedWorkflowData: undefined, doNotWaitToFinish: false }),
|
||||
);
|
||||
|
||||
expect(getVariablesSpy).toHaveBeenCalledWith(workflowId, undefined);
|
||||
// getBase backfills projectId from the workflow owner (mocked to
|
||||
// 'project-id-1' in this describe's beforeEach) before calling getVariables.
|
||||
expect(getVariablesSpy).toHaveBeenCalledWith(workflowId, 'project-id-1');
|
||||
});
|
||||
|
||||
describe('credential permission check routing', () => {
|
||||
@@ -1330,6 +1332,52 @@ describe('WorkflowExecuteAdditionalData', () => {
|
||||
|
||||
expect(additionalData.workflowSettings).toBe(workflowSettings);
|
||||
});
|
||||
|
||||
describe('projectId resolution', () => {
|
||||
const ownershipService = mockInstance(OwnershipService);
|
||||
|
||||
beforeEach(() => {
|
||||
ownershipService.getWorkflowProjectCached.mockReset();
|
||||
// Both this and the executeWorkflow/executeAgent describes call
|
||||
// mockInstance(OwnershipService), which each Container.set a fresh mock.
|
||||
// Re-bind ours so the source resolves it.
|
||||
Container.set(OwnershipService, ownershipService);
|
||||
});
|
||||
|
||||
it('backfills projectId from the workflow owner when missing', async () => {
|
||||
ownershipService.getWorkflowProjectCached.mockResolvedValue(
|
||||
mock<Project>({ id: 'owning-project-1' }),
|
||||
);
|
||||
|
||||
const additionalData = await getBase({ workflowId: 'workflow-1' });
|
||||
|
||||
expect(ownershipService.getWorkflowProjectCached).toHaveBeenCalledWith('workflow-1');
|
||||
expect(additionalData.projectId).toBe('owning-project-1');
|
||||
});
|
||||
|
||||
it('keeps the given projectId untouched when already present', async () => {
|
||||
const additionalData = await getBase({
|
||||
workflowId: 'workflow-1',
|
||||
projectId: 'given-project',
|
||||
});
|
||||
|
||||
expect(ownershipService.getWorkflowProjectCached).not.toHaveBeenCalled();
|
||||
expect(additionalData.projectId).toBe('given-project');
|
||||
});
|
||||
|
||||
it('leaves projectId unset when workflowId is missing', async () => {
|
||||
const additionalData = await getBase();
|
||||
|
||||
expect(ownershipService.getWorkflowProjectCached).not.toHaveBeenCalled();
|
||||
expect(additionalData.projectId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects when the workflow has no resolvable owning project', async () => {
|
||||
ownershipService.getWorkflowProjectCached.mockRejectedValue(new Error('not found'));
|
||||
|
||||
await expect(getBase({ workflowId: 'workflow-1' })).rejects.toThrow('not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeAgent', () => {
|
||||
|
||||
@@ -35,6 +35,11 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
describe('workflow-helpers', () => {
|
||||
const ownershipService = mockInstance(OwnershipService);
|
||||
ownershipService.getWorkflowProjectCached.mockResolvedValue(
|
||||
mock<Project>({ id: '1', name: 'project' }),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
mockInstance(VariablesService, {
|
||||
async getAllCached() {
|
||||
@@ -64,12 +69,6 @@ describe('workflow-helpers', () => {
|
||||
] as Variables[];
|
||||
},
|
||||
});
|
||||
|
||||
mockInstance(OwnershipService, {
|
||||
async getWorkflowProjectCached(_workflowId: string) {
|
||||
return { id: '1', name: 'project' } as unknown as Project;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVariables', () => {
|
||||
@@ -97,6 +96,12 @@ describe('workflow-helpers', () => {
|
||||
const variables = await getVariables(undefined, '1');
|
||||
expect(variables.VAR2).toBe('value1Project');
|
||||
});
|
||||
|
||||
it('should reject when the owning project cannot be resolved', async () => {
|
||||
ownershipService.getWorkflowProjectCached.mockRejectedValueOnce(new Error('not found'));
|
||||
|
||||
await expect(getVariables('1')).rejects.toThrow('not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
MANAGED_OAUTH_PINNED_FIELDS,
|
||||
} from '@/oauth/dcr-managed-fields';
|
||||
import { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config';
|
||||
import { PolicyEnforcementService } from '@/policy/policy-enforcement.service';
|
||||
import { AiGatewayService } from '@/services/ai-gateway.service';
|
||||
|
||||
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
||||
@@ -100,6 +101,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||
private readonly licenseState: LicenseState,
|
||||
private readonly externalSecretsConfig: ExternalSecretsConfig,
|
||||
private readonly aiGatewayService: AiGatewayService,
|
||||
private readonly policyEnforcementService: PolicyEnforcementService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -527,6 +529,15 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||
}
|
||||
|
||||
const credentialsEntity = await this.getCredentialsEntity(nodeCredentials, type);
|
||||
|
||||
// Validate against the executing project's policy before any decryption happens.
|
||||
await this.policyEnforcementService.enforceCredentialDecrypt({
|
||||
credentialType: type,
|
||||
credentialId: credentialsEntity.id,
|
||||
consumer: executeData ? { nodeType: executeData.node.type } : null,
|
||||
projectId: additionalData.projectId ?? null,
|
||||
});
|
||||
|
||||
const credentials = new Credentials(
|
||||
{ id: credentialsEntity.id, name: credentialsEntity.name },
|
||||
credentialsEntity.type,
|
||||
|
||||
@@ -789,6 +789,18 @@ export async function getBase({
|
||||
|
||||
const globalConfig = Container.get(GlobalConfig);
|
||||
|
||||
// Trigger-fired, webhook, and worker-queued executions build additionalData without
|
||||
// a `projectId`. Resolve it from the workflow's owning project so every downstream
|
||||
// consumer (e.g. policy enforcement) sees the executing project, same as
|
||||
// `executeAgent` already does locally for its own use. Left unguarded on purpose,
|
||||
// matching `getVariables`'s own pre-existing lookup below: an unresolvable owner
|
||||
// project fails execution setup, it isn't silently tolerated.
|
||||
if (!projectId && workflowId) {
|
||||
const { OwnershipService } = await import('@/services/ownership.service.js');
|
||||
const project = await Container.get(OwnershipService).getWorkflowProjectCached(workflowId);
|
||||
projectId = project?.id;
|
||||
}
|
||||
|
||||
const variables = await WorkflowHelpers.getVariables(workflowId, projectId);
|
||||
|
||||
const eventService = Container.get(EventService);
|
||||
|
||||
+17
@@ -100,6 +100,23 @@ describe('HookContext', () => {
|
||||
|
||||
expect(credentials).toEqual({ secret: 'token' });
|
||||
});
|
||||
|
||||
it('should surface the node to the credentials helper', async () => {
|
||||
credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' });
|
||||
credentialsHelper.isCredentialUsableByNode.mockReturnValue(true);
|
||||
|
||||
await hookContext.getCredentials<ICredentialDataDecryptedObject>(testCredentialType);
|
||||
|
||||
expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith(
|
||||
additionalData,
|
||||
expect.anything(),
|
||||
testCredentialType,
|
||||
mode,
|
||||
expect.objectContaining({ node }),
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNodeParameter', () => {
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ describe('LoadOptionsContext', () => {
|
||||
expect.anything(),
|
||||
testCredentialType,
|
||||
'internal',
|
||||
undefined,
|
||||
expect.objectContaining({ node }),
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
|
||||
+18
@@ -76,6 +76,24 @@ describe('PollContext', () => {
|
||||
|
||||
expect(credentials).toEqual({ secret: 'token' });
|
||||
});
|
||||
|
||||
it('should surface the node to the credentials helper', async () => {
|
||||
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
|
||||
credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' });
|
||||
credentialsHelper.isCredentialUsableByNode.mockReturnValue(true);
|
||||
|
||||
await pollContext.getCredentials<ICredentialDataDecryptedObject>(testCredentialType);
|
||||
|
||||
expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith(
|
||||
additionalData,
|
||||
expect.anything(),
|
||||
testCredentialType,
|
||||
mode,
|
||||
expect.objectContaining({ node }),
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNodeParameter', () => {
|
||||
|
||||
+18
@@ -76,6 +76,24 @@ describe('TriggerContext', () => {
|
||||
|
||||
expect(credentials).toEqual({ secret: 'token' });
|
||||
});
|
||||
|
||||
it('should surface the node to the credentials helper', async () => {
|
||||
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
|
||||
credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' });
|
||||
credentialsHelper.isCredentialUsableByNode.mockReturnValue(true);
|
||||
|
||||
await triggerContext.getCredentials<ICredentialDataDecryptedObject>(testCredentialType);
|
||||
|
||||
expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith(
|
||||
additionalData,
|
||||
expect.anything(),
|
||||
testCredentialType,
|
||||
mode,
|
||||
expect.objectContaining({ node }),
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNodeParameter', () => {
|
||||
|
||||
+18
@@ -140,6 +140,24 @@ describe('WebhookContext', () => {
|
||||
|
||||
expect(credentials).toEqual({ secret: 'token' });
|
||||
});
|
||||
|
||||
it('should surface the node to the credentials helper', async () => {
|
||||
nodeTypes.getByNameAndVersion.mockReturnValue(nodeType);
|
||||
credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' });
|
||||
credentialsHelper.isCredentialUsableByNode.mockReturnValue(true);
|
||||
|
||||
await webhookContext.getCredentials<ICredentialDataDecryptedObject>(testCredentialType);
|
||||
|
||||
expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith(
|
||||
additionalData,
|
||||
expect.anything(),
|
||||
testCredentialType,
|
||||
mode,
|
||||
expect.objectContaining({ node }),
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBodyData', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IExecuteData,
|
||||
INode,
|
||||
IHookFunctions,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
@@ -36,7 +37,12 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions
|
||||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await this._getCredentials<T>(type);
|
||||
// No real task run backs a webhook-registration hook, so this only exists to
|
||||
// surface `node` to the credentials helper (e.g. for policy checks) — `data`/
|
||||
// `source` are unused.
|
||||
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
|
||||
|
||||
return await this._getCredentials<T>(type, executeData);
|
||||
}
|
||||
|
||||
getNodeWebhookUrl(name: WebhookType): string | undefined {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import get from 'lodash/get';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IExecuteData,
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
ILoadOptionsFunctions,
|
||||
@@ -44,7 +45,12 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
|
||||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await this._getCredentials<T>(type);
|
||||
// No real task run backs design-time parameter loading, so this only exists to
|
||||
// surface `node` to the credentials helper (e.g. for policy checks) — `data`/`source`
|
||||
// are unused.
|
||||
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
|
||||
|
||||
return await this._getCredentials<T>(type, executeData);
|
||||
}
|
||||
|
||||
getCurrentNodeParameter(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createDeferredPromise } from '@n8n/utils/promise/deferred-promise';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IDataObject,
|
||||
IExecuteData,
|
||||
INode,
|
||||
IPollFunctions,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
@@ -66,6 +67,10 @@ export class PollContext extends NodeExecutionContext implements IPollFunctions
|
||||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await this._getCredentials<T>(type);
|
||||
// No real task run backs a poll, so this only exists to surface `node` to
|
||||
// the credentials helper (e.g. for policy checks) — `data`/`source` are unused.
|
||||
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
|
||||
|
||||
return await this._getCredentials<T>(type, executeData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createDeferredPromise } from '@n8n/utils/promise/deferred-promise';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IExecuteData,
|
||||
INode,
|
||||
ITriggerFunctions,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
@@ -65,6 +66,10 @@ export class TriggerContext extends NodeExecutionContext implements ITriggerFunc
|
||||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await this._getCredentials<T>(type);
|
||||
// No real task run backs a trigger, so this only exists to surface `node` to
|
||||
// the credentials helper (e.g. for policy checks) — `data`/`source` are unused.
|
||||
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
|
||||
|
||||
return await this._getCredentials<T>(type, executeData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,11 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc
|
||||
}
|
||||
|
||||
async getCredentials<T extends object = ICredentialDataDecryptedObject>(type: string) {
|
||||
return await this._getCredentials<T>(type);
|
||||
// No real task run backs a webhook call, so this only exists to surface `node`
|
||||
// to the credentials helper (e.g. for policy checks) — `data`/`source` are unused.
|
||||
const executeData: IExecuteData = { data: {}, node: this.node, source: null };
|
||||
|
||||
return await this._getCredentials<T>(type, executeData);
|
||||
}
|
||||
|
||||
getBodyData() {
|
||||
|
||||
Reference in New Issue
Block a user