n8n/packages/cli/src/credentials-helper.ts
n8n-assistant[bot] eeaf84f756
chore: Bundle/2.x (#40030)
Co-authored-by: Matsu <matias.huhta@n8n.io>
2026-10-01 09:50:06 +03:00

919 lines
29 KiB
TypeScript

import { SYSTEM_RESOLVER_ID } from '@n8n/api-types';
import { LicenseState } from '@n8n/backend-common';
import type { CredentialsEntity, ICredentialsDb } from '@n8n/db';
import {
CredentialsRepository,
isEntityNotFoundError,
SecretsProviderConnectionRepository,
} from '@n8n/db';
import { Service } from '@n8n/di';
import { Credentials, FULL_ACCESS_NODE_TYPES, getAdditionalKeys } from 'n8n-core';
import type {
CredentialInformation,
ICredentialDataDecryptedObject,
ICredentialType,
ICredentialsExpressionResolveValues,
IHttpRequestOptions,
INode,
INodeCredentialsDetails,
INodeParameters,
INodeProperties,
IRequestOptionsSimplified,
IWorkflowDataProxyAdditionalKeys,
WorkflowExecuteMode,
IHttpRequestHelper,
IWorkflowExecuteAdditionalData,
IExecuteData,
IGetDecryptedCredentialsOptions,
IDataObject,
} from 'n8n-workflow';
import {
ICredentialsHelper,
NodeHelpers,
OPEN_AI_API_CREDENTIAL_TYPE,
Workflow,
UnexpectedError,
UserError,
classifyTriggerIdentity,
getCredentialOwnRequestAllowedDomains,
isExpression,
jsonParse,
} from 'n8n-workflow';
import { CredentialTypes } from '@/credential-types';
import { CredentialsOverwrites } from '@/credentials-overwrites';
import { DCR_MANAGED_CREDENTIAL_FIELDS, OAUTH_PINNED_FIELDS } from '@/oauth/dcr-managed-fields';
import { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config';
import type { PolicyActor } from '@/policy/policy-enforcement-backend';
import { PolicyEnforcementService } from '@/policy/policy-enforcement.service';
import { AiGatewayService } from '@/services/ai-gateway.service';
import { DynamicCredentialsProxy } from './credentials/dynamic-credentials-proxy';
import { createMockNodeTypes } from './credentials/mock-node-types';
import { CredentialMissingIdError } from './errors/credential-missing-id.error';
import { CredentialNotFoundError } from './errors/credential-not-found.error';
import { UnsupportedEndUserCredentialTriggerError } from './errors/unsupported-end-user-credential-trigger.error';
/**
* Applies the credential's allowlist to the requests its `preAuthentication` hook issues.
*
* Narrowed to the single method `IHttpRequestHelper` declares, rather than spreading the
* node's wider helper bag, so a hook cannot reach an unwrapped request method on it.
*
* Read per request rather than up front: `runPreAuthentication` runs on every OAuth2
* request for hooks that only transform token data in memory, and an empty `'domains'`
* list must not fail those.
*/
function restrictToCredentialDomains(
helpers: IHttpRequestHelper,
credentials: ICredentialDataDecryptedObject,
): IHttpRequestHelper {
return {
helpers: {
httpRequest: async (requestOptions: IHttpRequestOptions): Promise<unknown> => {
const allowedDomains = getCredentialOwnRequestAllowedDomains(credentials);
if (allowedDomains === undefined) {
return await helpers.helpers.httpRequest(requestOptions);
}
// A request carries one allowlist, and honouring either side alone could widen
// what the other permits, so refuse rather than pick.
if (requestOptions.allowedDomains !== undefined) {
throw new UserError(
'This credential restricts requests to specific domains, which cannot be combined with the domains its authentication step asks for.',
);
}
return await helpers.helpers.httpRequest({ ...requestOptions, allowedDomains });
},
},
};
}
const mockNode = {
name: '',
typeVersion: 1,
type: 'mock',
position: [0, 0],
parameters: {},
} as INode;
const { nodeTypes: mockNodeTypes } = createMockNodeTypes();
const INVALID_JSON_VALUE = Symbol('invalidJsonValue');
/** A run names no user: the starter is not known reliably on every path, so a run never guesses. */
function decryptActor({ executionId, userId }: IWorkflowExecuteAdditionalData): PolicyActor {
if (executionId) return { kind: 'system', reason: 'execution', executionId };
if (userId) return { kind: 'user', user: { id: userId } };
return { kind: 'system', reason: 'execution' };
}
@Service()
export class CredentialsHelper extends ICredentialsHelper {
constructor(
private readonly credentialTypes: CredentialTypes,
private readonly credentialsOverwrites: CredentialsOverwrites,
private readonly credentialsRepository: CredentialsRepository,
private readonly dynamicCredentialsProxy: DynamicCredentialsProxy,
private readonly secretsProviderConnectionRepository: SecretsProviderConnectionRepository,
private readonly licenseState: LicenseState,
private readonly externalSecretsConfig: ExternalSecretsConfig,
private readonly aiGatewayService: AiGatewayService,
private readonly policyEnforcementService: PolicyEnforcementService,
) {
super();
}
/**
* Add the required authentication information to the request
*/
async authenticate(
credentials: ICredentialDataDecryptedObject,
typeName: string,
incomingRequestOptions: IHttpRequestOptions | IRequestOptionsSimplified,
workflow?: Workflow,
node?: INode,
): Promise<IHttpRequestOptions> {
const requestOptions = incomingRequestOptions;
const credentialType = this.credentialTypes.getByName(typeName);
if (credentialType.authenticate) {
if (typeof credentialType.authenticate === 'function') {
// Special authentication function is defined
return await credentialType.authenticate(
credentials,
requestOptions as IHttpRequestOptions,
);
}
if (typeof credentialType.authenticate === 'object') {
if (!workflow || !node) {
throw new UnexpectedError(
'Workflow and node are required for declarative credential authentication',
);
}
// Predefined authentication method
let keyResolved: string;
let valueResolved: string;
const { authenticate } = credentialType;
if (requestOptions.headers === undefined) {
requestOptions.headers = {};
}
if (authenticate.type === 'generic') {
Object.entries(authenticate.properties).forEach(([outerKey, outerValue]) => {
Object.entries(outerValue).forEach(([key, value]) => {
keyResolved = this.resolveValue(key, { $credentials: credentials }, workflow, node);
valueResolved = this.resolveValue(
value as string,
{ $credentials: credentials },
workflow,
node,
);
// @ts-expect-error dynamic key on request options
if (!requestOptions[outerKey]) {
// @ts-expect-error dynamic key on request options
requestOptions[outerKey] = {};
}
// @ts-expect-error dynamic key on request options
requestOptions[outerKey][keyResolved] = valueResolved;
});
});
}
}
}
return requestOptions as IHttpRequestOptions;
}
async preAuthentication(
helpers: IHttpRequestHelper,
credentials: ICredentialDataDecryptedObject,
typeName: string,
node: INode,
credentialsExpired: boolean,
): Promise<ICredentialDataDecryptedObject | undefined> {
const credentialType = this.credentialTypes.getByName(typeName);
const expirableProperty = credentialType.properties.find(
(property) => property.type === 'hidden' && property?.typeOptions?.expirable === true,
);
if (expirableProperty?.name === undefined) {
return undefined;
}
// check if the node is the mockup node used for testing
// if so, it means this is a credential test and not normal node execution
const isTestingCredentials =
node?.parameters?.temp === '' && node?.type === 'n8n-nodes-base.noOp';
if (credentialType.preAuthentication) {
if (typeof credentialType.preAuthentication === 'function') {
// if the expirable property is empty in the credentials
// or are expired, call pre authentication method
// or the credentials are being tested
if (
credentials[expirableProperty?.name] === '' ||
credentialsExpired ||
isTestingCredentials
) {
const output = await credentialType.preAuthentication.call(
restrictToCredentialDomains(helpers, credentials),
credentials,
);
// if there is data in the output, make sure the returned
// property is the expirable property
// else the database will not get updated
if (output[expirableProperty.name] === undefined) {
return undefined;
}
if (node.credentials) {
const nodeCredentials = node.credentials[credentialType.name];
// Cache the freshly-fetched token onto the raw stored credentials, but
// never overwrite a field the user stored as an expression with the value
// it resolved to this run — otherwise later runs reuse a stale static value.
const storedData = await (
await this.getCredentials(nodeCredentials, credentialType.name)
).getData();
const dataToPersist: ICredentialDataDecryptedObject = { ...storedData };
for (const [key, value] of Object.entries(output as ICredentialDataDecryptedObject)) {
if (key === expirableProperty.name || !isExpression(storedData[key])) {
dataToPersist[key] = value;
}
}
await this.updateCredentials(nodeCredentials, credentialType.name, dataToPersist);
return Object.assign(credentials, output);
}
}
}
}
return undefined;
}
/**
* Invokes a credential's `preAuthentication` hook for in-memory transformation,
* without the expirable-property guard or DB persistence. Used by `requestOAuth2`
* to transform `oauthTokenData` on every request (e.g. extracting a claim from a
* decrypted JWE/JWT) without writing to the database on every call.
*/
async runPreAuthentication(
helpers: IHttpRequestHelper,
credentials: ICredentialDataDecryptedObject,
typeName: string,
): Promise<ICredentialDataDecryptedObject | undefined> {
const credentialType = this.credentialTypes.getByName(typeName);
if (typeof credentialType.preAuthentication !== 'function') {
return undefined;
}
const output = await credentialType.preAuthentication.call(
restrictToCredentialDomains(helpers, credentials),
credentials,
);
return (output as ICredentialDataDecryptedObject) ?? undefined;
}
/**
* Resolves the given value in case it is an expression
*/
private resolveValue(
parameterValue: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
workflow: Workflow,
node: INode,
): string {
if (!isExpression(parameterValue)) {
return parameterValue;
}
const returnValue = workflow.expression.getSimpleParameterValue(
node,
parameterValue,
'internal',
additionalKeys,
undefined,
'',
);
if (!returnValue) {
return '';
}
return returnValue.toString();
}
/**
* Returns all parent types of the given credential type
*/
getParentTypes(typeName: string): string[] {
return this.credentialTypes.getParentTypes(typeName);
}
/**
* Returns the credentials instance
*/
async getCredentials(
nodeCredential: INodeCredentialsDetails,
type: string,
): Promise<Credentials> {
const credential = await this.getCredentialsEntity(nodeCredential, type);
return new Credentials(
{ id: credential.id, name: credential.name },
credential.type,
credential.data,
);
}
/**
* Loads the credentials entity from the database
*/
private async getCredentialsEntity(
nodeCredential: INodeCredentialsDetails,
type: string,
): Promise<CredentialsEntity> {
if (!nodeCredential.id) {
throw new CredentialMissingIdError(nodeCredential.name, type);
}
let credential: CredentialsEntity;
try {
credential = await this.credentialsRepository.findOneByOrFail({
id: nodeCredential.id,
type,
});
} catch (error) {
if (isEntityNotFoundError(error)) {
throw new CredentialNotFoundError(nodeCredential.id, type);
}
throw error;
}
// Keep non-project credentials blocked even if an earlier access check is bypassed.
if (credential.usageScope !== 'project') {
throw new UserError('This credential cannot be used in workflows');
}
return credential;
}
isCredentialUsableByNode(credentialType: string, nodeType: string): boolean {
let typeDef: ICredentialType;
try {
typeDef = this.credentialTypes.getByName(credentialType);
} catch {
// Unknown credential type — let downstream code surface the real error.
return true;
}
if (!typeDef.restrictToSupportedNodes) return true;
// `typeDef.supportedNodes` from the loader holds short node names; the FQ
// list (matching the `nodeType` we receive) is exposed via `getSupportedNodes`.
return this.credentialTypes.getSupportedNodes(credentialType).includes(nodeType);
}
/**
* Returns all the properties of the credentials with the given name
*/
getCredentialsProperties(type: string): INodeProperties[] {
const credentialTypeData = this.credentialTypes.getByName(type);
if (credentialTypeData === undefined) {
throw new UnexpectedError('Unknown credential type', { tags: { credentialType: type } });
}
if (credentialTypeData.extends === undefined) {
// Manually add the special OAuth parameter which stores
// data like access- and refresh-token
if (['oAuth1Api', 'oAuth2Api'].includes(type)) {
return [
...credentialTypeData.properties,
{
displayName: 'oauthTokenData',
name: 'oauthTokenData',
type: 'json',
required: false,
default: {},
},
];
}
return credentialTypeData.properties;
}
const combineProperties = [] as INodeProperties[];
for (const credentialsTypeName of credentialTypeData.extends) {
const mergeCredentialProperties = this.getCredentialsProperties(credentialsTypeName);
NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
}
// The properties defined on the parent credentials take precedence
NodeHelpers.mergeNodeProperties(combineProperties, credentialTypeData.properties);
return combineProperties;
}
private resolveCredentialExpressions(
workflow: Workflow,
credentialsProperties: INodeProperties[],
decryptedData: ICredentialDataDecryptedObject,
mode: WorkflowExecuteMode,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
): ICredentialDataDecryptedObject {
try {
return workflow.expression.getComplexParameterValue(
mockNode,
decryptedData as INodeParameters,
mode,
additionalKeys,
undefined,
undefined,
decryptedData,
) as ICredentialDataDecryptedObject;
} catch (error) {
// Non-execution contexts may not have runtime data for optional credential fields.
// Resolve per field so opted-in fields can be omitted without losing required data.
return this.resolveCredentialExpressionsByProperty(
workflow,
credentialsProperties,
decryptedData,
mode,
additionalKeys,
error,
);
}
}
private resolveCredentialExpressionsByProperty(
workflow: Workflow,
credentialsProperties: INodeProperties[],
decryptedData: ICredentialDataDecryptedObject,
mode: WorkflowExecuteMode,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
originalError: unknown,
): ICredentialDataDecryptedObject {
const propertiesByName = new Map(
credentialsProperties.map((property) => [property.name, property]),
);
const resolvedData: ICredentialDataDecryptedObject = {};
for (const [propertyName, propertyValue] of Object.entries(decryptedData)) {
try {
const resolvedProperty = workflow.expression.getComplexParameterValue(
mockNode,
{ [propertyName]: propertyValue } as INodeParameters,
mode,
additionalKeys,
undefined,
undefined,
decryptedData,
) as ICredentialDataDecryptedObject;
if (Object.prototype.hasOwnProperty.call(resolvedProperty, propertyName)) {
resolvedData[propertyName] = resolvedProperty[propertyName];
}
} catch {
const credentialProperty = propertiesByName.get(propertyName);
if (credentialProperty?.typeOptions?.ignoreCredentialExpressionResolveError === true) {
continue;
}
throw originalError;
}
}
return resolvedData;
}
private parseJsonLeafExpressionFields(
credentialsProperties: INodeProperties[],
decryptedData: ICredentialDataDecryptedObject,
): Map<string, string> {
const parsedFields = new Map<string, string>();
for (const property of credentialsProperties) {
if (!property.typeOptions?.resolveCredentialJsonLeaves) continue;
const value = decryptedData[property.name];
if (typeof value !== 'string' || value === '') continue;
const parsed = jsonParse<CredentialInformation | typeof INVALID_JSON_VALUE>(value, {
fallbackValue: INVALID_JSON_VALUE,
});
if (parsed === INVALID_JSON_VALUE) continue;
parsedFields.set(property.name, value);
decryptedData[property.name] = parsed;
}
return parsedFields;
}
private stringifyJsonLeafExpressionFields(
decryptedData: ICredentialDataDecryptedObject,
parsedFields: Map<string, string>,
) {
for (const [propertyName, originalValue] of parsedFields) {
const serializedValue = JSON.stringify(decryptedData[propertyName]);
decryptedData[propertyName] = serializedValue ?? originalValue;
}
}
private assertTriggerSupportsEndUserCredential(
credentialsEntity: CredentialsEntity,
additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData | undefined,
): void {
if (!executeData) return;
const resolverId =
credentialsEntity.resolverId ??
this.dynamicCredentialsProxy.getEffectiveResolverId(additionalData.workflowSettings);
if (!resolverId) return;
const isSystemResolver = resolverId === SYSTEM_RESOLVER_ID;
const { providesN8nIdentity, providesExternalIdentity } = classifyTriggerIdentity(
executeData.node.type,
executeData.node.parameters,
);
const supportsResolver = isSystemResolver ? providesN8nIdentity : providesExternalIdentity;
if (!supportsResolver) {
throw new UnsupportedEndUserCredentialTriggerError(isSystemResolver ? 'system' : 'custom');
}
}
/**
* Returns the decrypted credential data with applied overwrites
*/
async getDecrypted(
additionalData: IWorkflowExecuteAdditionalData,
nodeCredentials: INodeCredentialsDetails,
type: string,
mode: WorkflowExecuteMode,
executeData?: IExecuteData,
raw?: boolean,
expressionResolveValues?: ICredentialsExpressionResolveValues,
options?: IGetDecryptedCredentialsOptions & { actor?: PolicyActor },
): Promise<ICredentialDataDecryptedObject> {
// Sub-nodes, such as a chat model connected to a chain or agent, inherit executeData.node
// from their parent. Prefer expressionResolveValues.node when present: it is always
// the node making this call to resolve credentials.
const consumerNode = expressionResolveValues?.node ?? executeData?.node;
if (nodeCredentials.__aiGatewayManaged) {
const { userId, workflowId, projectId, executionId } = additionalData;
return await this.aiGatewayService.getSyntheticCredential({
credentialType: type,
userId,
workflowId,
projectId,
executionId,
node: consumerNode,
});
}
const credentialsEntity = await this.getCredentialsEntity(nodeCredentials, type);
// Managed OpenAI credentials are unavailable to nodes that can request undeclared types.
if (
credentialsEntity.isManaged &&
type === OPEN_AI_API_CREDENTIAL_TYPE &&
consumerNode &&
FULL_ACCESS_NODE_TYPES.has(consumerNode.type)
) {
throw new UserError('Managed credentials are not supported by this node');
}
// Validate against the executing project's policy before any decryption happens.
await this.policyEnforcementService.enforceCredentialDecrypt(
{
credentialType: type,
credentialId: credentialsEntity.id,
consumer: consumerNode ? { nodeType: consumerNode.type } : null,
projectId: additionalData.projectId ?? null,
},
options?.actor ?? decryptActor(additionalData),
);
const credentials = new Credentials(
{ id: credentialsEntity.id, name: credentialsEntity.name },
credentialsEntity.type,
credentialsEntity.data,
);
let decryptedDataOriginal = await credentials.getData();
// In manual or internal mode (or when the root execution is manual, e.g. a subworkflow
// called from a manual parent), skip dynamic resolution unless a credentials context is
// present or a trigger explicitly requests the credential. Canvas action-node tests have
// no incoming request, so they fall back to static data for easier developer testing.
// Trigger contexts opt in so a missing identity produces an actionable error instead.
// Internal mode is used by OAuth authorize/revoke flows which are not actual workflow
// executions and should not trigger dynamic resolution.
// For all other modes (especially production), always attempt resolution —
// missing credentials will surface an error rather than silently falling back to
// static data.
const effectiveMode = additionalData.rootExecutionMode ?? mode;
const skipDynamicResolution = effectiveMode === 'manual' || effectiveMode === 'internal';
const isManualTriggerCredentialRequest =
effectiveMode === 'manual' &&
options?.credentialUsage === 'trigger' &&
credentialsEntity.isResolvable;
const shouldAttemptDynamicResolution =
additionalData.executionContext?.credentials !== undefined ||
isManualTriggerCredentialRequest ||
!skipDynamicResolution;
// Mark that this execution attempted to run with a private credential before
// resolution is attempted, so the flag survives even when resolution throws
// (e.g. the running user has not connected the credential). Telemetry-only;
// the redaction layer relies on `currentNodeUsedDynamicCredentials` instead.
if (shouldAttemptDynamicResolution && credentialsEntity.isResolvable) {
additionalData.currentNodeAttemptedDynamicCredentials = true;
}
if (
isManualTriggerCredentialRequest &&
additionalData.executionContext?.credentials === undefined
) {
this.assertTriggerSupportsEndUserCredential(credentialsEntity, additionalData, executeData);
}
if (shouldAttemptDynamicResolution) {
// Resolve dynamic credentials if configured (EE feature)
const resolveResult = await this.dynamicCredentialsProxy.resolveIfNeeded(
{
id: credentialsEntity.id,
name: credentialsEntity.name,
type: credentialsEntity.type,
isResolvable: credentialsEntity.isResolvable,
resolverId: credentialsEntity.resolverId ?? undefined,
},
decryptedDataOriginal,
additionalData.executionContext,
additionalData.workflowSettings,
additionalData.executionId,
);
decryptedDataOriginal = resolveResult.data;
if (resolveResult.isDynamic) {
additionalData.currentNodeUsedDynamicCredentials = true;
if (resolveResult.resolvedUserId) {
additionalData.dynamicCredentialsResolvedUserId = resolveResult.resolvedUserId;
}
}
}
if (raw === true) {
return decryptedDataOriginal;
}
if (
this.licenseState.isExternalSecretsLicensed() &&
this.externalSecretsConfig.externalSecretsForProjects
) {
const accessibleProviderKeys =
await this.secretsProviderConnectionRepository.findAllAccessibleProviderKeysByCredentialId(
credentialsEntity.id,
);
additionalData.externalSecretProviderKeysAccessibleByCredential = new Set(
accessibleProviderKeys,
);
}
return await this.applyDefaultsAndOverwrites(
additionalData,
decryptedDataOriginal,
type,
mode,
executeData,
expressionResolveValues,
);
}
/**
* Applies credential default data and overwrites
*/
async applyDefaultsAndOverwrites(
additionalData: IWorkflowExecuteAdditionalData,
decryptedDataOriginal: ICredentialDataDecryptedObject,
type: string,
mode: WorkflowExecuteMode,
executeData?: IExecuteData,
expressionResolveValues?: ICredentialsExpressionResolveValues,
): Promise<ICredentialDataDecryptedObject> {
const credentialsProperties = this.getCredentialsProperties(type);
// The credential type owns hidden OAuth endpoint/flow fields. Drop stored values so
// applyOverwrite (admin overwrite) and getNodeParameters (type default) fill them.
const storedData = { ...decryptedDataOriginal };
for (const field of OAUTH_PINNED_FIELDS) {
if (credentialsProperties.some((p) => p.name === field && p.type === 'hidden')) {
delete storedData[field];
}
}
// Load and apply the credentials overwrites if any exist
const dataWithOverwrites = this.credentialsOverwrites.applyOverwrite(type, storedData);
// Add the default credential values
let decryptedData = NodeHelpers.getNodeParameters(
credentialsProperties,
dataWithOverwrites as INodeParameters,
true,
false,
null,
null,
) as ICredentialDataDecryptedObject;
if (decryptedDataOriginal.oauthTokenData !== undefined) {
// The OAuth data gets removed as it is not defined specifically as a parameter
// on the credentials so add it back in case it was set
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
}
if (
decryptedData.allowedHttpRequestDomains === undefined &&
decryptedDataOriginal.allowedHttpRequestDomains !== undefined
) {
decryptedData.allowedHttpRequestDomains = decryptedDataOriginal.allowedHttpRequestDomains;
}
if (
decryptedData.allowedDomains === undefined &&
decryptedDataOriginal.allowedDomains !== undefined
) {
decryptedData.allowedDomains = decryptedDataOriginal.allowedDomains;
}
// When using dynamic client registration, OAuth fields negotiated at runtime
// are not shown in the UI, so we need to copy them from the original data.
if (decryptedData.useDynamicClientRegistration) {
for (const field of DCR_MANAGED_CREDENTIAL_FIELDS) {
decryptedData[field] = decryptedDataOriginal[field];
}
}
const parsedJsonLeafExpressionFields = this.parseJsonLeafExpressionFields(
credentialsProperties,
decryptedData,
);
const additionalKeys = getAdditionalKeys(additionalData, mode, null, {
isCredential: true,
});
if (expressionResolveValues) {
try {
decryptedData = expressionResolveValues.workflow.expression.getParameterValue(
decryptedData as INodeParameters,
expressionResolveValues.runExecutionData,
expressionResolveValues.runIndex,
expressionResolveValues.itemIndex,
expressionResolveValues.node.name,
expressionResolveValues.connectionInputData,
mode,
additionalKeys,
executeData,
false,
decryptedData,
) as ICredentialDataDecryptedObject;
} catch (e) {
e.message += ' [Error resolving credentials]';
throw e;
}
} else {
const workflow = new Workflow({
nodes: [mockNode],
connections: {},
active: false,
nodeTypes: mockNodeTypes,
});
// Resolve expressions if any are set
await workflow.expression.acquireIsolate();
try {
decryptedData = this.resolveCredentialExpressions(
workflow,
credentialsProperties,
decryptedData,
mode,
additionalKeys,
);
} finally {
await workflow.expression.releaseIsolate();
}
}
this.stringifyJsonLeafExpressionFields(decryptedData, parsedJsonLeafExpressionFields);
return decryptedData;
}
/**
* Updates credentials in the database
*/
async updateCredentials(
nodeCredentials: INodeCredentialsDetails,
type: string,
data: ICredentialDataDecryptedObject,
): Promise<void> {
const credentials = await this.getCredentials(nodeCredentials, type);
await credentials.setData(data);
// Ciphertext only. `name` and `type` would be written back unchanged, and a payload
// that cannot carry `type` keeps this off the sealed `credentialSave` path.
const newCredentialsData: Pick<ICredentialsDb, 'data' | 'updatedAt'> = {
data: credentials.getDataToSave().data,
updatedAt: new Date(),
};
// Save the credentials in DB
const findQuery = {
id: credentials.id,
type,
};
await this.credentialsRepository.update(findQuery, newCredentialsData);
}
/**
* Updates credential's oauth token data in the database
*/
async updateCredentialsOauthTokenData(
nodeCredentials: INodeCredentialsDetails,
type: string,
data: ICredentialDataDecryptedObject,
additionalData: IWorkflowExecuteAdditionalData,
): Promise<void> {
const credentialsEntity = await this.getCredentialsEntity(nodeCredentials, type);
const resolverId =
credentialsEntity.resolverId ??
this.dynamicCredentialsProxy.getEffectiveResolverId(additionalData.workflowSettings);
if (
credentialsEntity.isResolvable &&
resolverId &&
additionalData.executionContext?.credentials
) {
const credentials = await this.getCredentials(nodeCredentials, type);
const staticData = await credentials.getData();
await this.dynamicCredentialsProxy.storeOAuthTokenDataIfNeeded(
{
id: credentialsEntity.id,
name: credentialsEntity.name,
type: credentialsEntity.type,
isResolvable: credentialsEntity.isResolvable,
resolverId,
},
data.oauthTokenData as IDataObject,
additionalData.executionContext,
staticData,
additionalData.workflowSettings,
additionalData.executionId,
);
return;
}
const credentials = await this.getCredentials(nodeCredentials, type);
await credentials.updateData({ oauthTokenData: data.oauthTokenData });
// Ciphertext only. `name` and `type` would be written back unchanged, and a payload
// that cannot carry `type` keeps this off the sealed `credentialSave` path.
const newCredentialsData: Pick<ICredentialsDb, 'data' | 'updatedAt'> = {
data: credentials.getDataToSave().data,
updatedAt: new Date(),
};
// Save the credentials in DB
const findQuery = {
id: credentials.id,
type,
};
await this.credentialsRepository.update(findQuery, newCredentialsData);
}
}
export function createCredentialsFromCredentialsEntity(
credential: ICredentialsDb,
encrypt = false,
): Credentials {
const { id, name, type, data } = credential;
if (encrypt) {
return new Credentials({ id: null, name }, type);
}
return new Credentials({ id, name }, type, data);
}