feat(core): Move execution permission checks earlier in the lifecycle (#8677)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-02-21 14:47:02 +01:00 committed by GitHub
parent a573146135
commit 059d281fd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 139 additions and 201 deletions

View file

@ -22,10 +22,10 @@ export class PermissionChecker {
/** /**
* Check if a user is permitted to execute a workflow. * Check if a user is permitted to execute a workflow.
*/ */
async check(workflow: Workflow, userId: string) { async check(workflowId: string, userId: string, nodes: INode[]) {
// allow if no nodes in this workflow use creds // allow if no nodes in this workflow use creds
const credIdsToNodes = this.mapCredIdsToNodes(workflow); const credIdsToNodes = this.mapCredIdsToNodes(nodes);
const workflowCredIds = Object.keys(credIdsToNodes); const workflowCredIds = Object.keys(credIdsToNodes);
@ -46,8 +46,8 @@ export class PermissionChecker {
let workflowUserIds = [userId]; let workflowUserIds = [userId];
if (workflow.id && isSharingEnabled) { if (workflowId && isSharingEnabled) {
workflowUserIds = await this.sharedWorkflowRepository.getSharedUserIds(workflow.id); workflowUserIds = await this.sharedWorkflowRepository.getSharedUserIds(workflowId);
} }
const accessibleCredIds = isSharingEnabled const accessibleCredIds = isSharingEnabled
@ -62,7 +62,7 @@ export class PermissionChecker {
const inaccessibleCredId = inaccessibleCredIds[0]; const inaccessibleCredId = inaccessibleCredIds[0];
const nodeToFlag = credIdsToNodes[inaccessibleCredId][0]; const nodeToFlag = credIdsToNodes[inaccessibleCredId][0];
throw new CredentialAccessError(nodeToFlag, inaccessibleCredId, workflow); throw new CredentialAccessError(nodeToFlag, inaccessibleCredId, workflowId);
} }
async checkSubworkflowExecutePolicy( async checkSubworkflowExecutePolicy(
@ -129,25 +129,22 @@ export class PermissionChecker {
} }
} }
private mapCredIdsToNodes(workflow: Workflow) { private mapCredIdsToNodes(nodes: INode[]) {
return Object.values(workflow.nodes).reduce<{ [credentialId: string]: INode[] }>( return nodes.reduce<{ [credentialId: string]: INode[] }>((map, node) => {
(map, node) => { if (node.disabled || !node.credentials) return map;
if (node.disabled || !node.credentials) return map;
Object.values(node.credentials).forEach((cred) => { Object.values(node.credentials).forEach((cred) => {
if (!cred.id) { if (!cred.id) {
throw new NodeOperationError(node, 'Node uses invalid credential', { throw new NodeOperationError(node, 'Node uses invalid credential', {
description: 'Please recreate the credential.', description: 'Please recreate the credential.',
level: 'warning', level: 'warning',
}); });
} }
map[cred.id] = map[cred.id] ? [...map[cred.id], node] : [node]; map[cred.id] = map[cred.id] ? [...map[cred.id], node] : [node];
}); });
return map; return map;
}, }, {});
{},
);
} }
} }

View file

@ -389,7 +389,7 @@ export function hookFunctionsPreExecute(): IWorkflowExecuteHooks {
* Returns hook functions to save workflow execution and call error workflow * Returns hook functions to save workflow execution and call error workflow
* *
*/ */
function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { function hookFunctionsSave(): IWorkflowExecuteHooks {
const logger = Container.get(Logger); const logger = Container.get(Logger);
const internalHooks = Container.get(InternalHooks); const internalHooks = Container.get(InternalHooks);
const eventsService = Container.get(EventsService); const eventsService = Container.get(EventsService);
@ -418,7 +418,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
await restoreBinaryDataId(fullRunData, this.executionId, this.mode); await restoreBinaryDataId(fullRunData, this.executionId, this.mode);
const isManualMode = [this.mode, parentProcessMode].includes('manual'); const isManualMode = this.mode === 'manual';
try { try {
if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) { if (!isManualMode && isWorkflowIdValid(this.workflowData.id) && newStaticData) {
@ -795,7 +795,11 @@ async function executeWorkflow(
let data; let data;
try { try {
await Container.get(PermissionChecker).check(workflow, additionalData.userId); await Container.get(PermissionChecker).check(
workflowData.id,
additionalData.userId,
workflowData.nodes,
);
await Container.get(PermissionChecker).checkSubworkflowExecutePolicy( await Container.get(PermissionChecker).checkSubworkflowExecutePolicy(
workflow, workflow,
options.parentWorkflowId, options.parentWorkflowId,
@ -809,7 +813,6 @@ async function executeWorkflow(
runData.executionMode, runData.executionMode,
executionId, executionId,
workflowData, workflowData,
{ parentProcessMode: additionalData.hooks!.mode },
); );
additionalDataIntegrated.executionId = executionId; additionalDataIntegrated.executionId = executionId;
@ -1011,10 +1014,8 @@ function getWorkflowHooksIntegrated(
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
executionId: string, executionId: string,
workflowData: IWorkflowBase, workflowData: IWorkflowBase,
optionalParameters?: IWorkflowHooksOptionalParameters,
): WorkflowHooks { ): WorkflowHooks {
optionalParameters = optionalParameters || {}; const hookFunctions = hookFunctionsSave();
const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode);
const preExecuteFunctions = hookFunctionsPreExecute(); const preExecuteFunctions = hookFunctionsPreExecute();
for (const key of Object.keys(preExecuteFunctions)) { for (const key of Object.keys(preExecuteFunctions)) {
if (hookFunctions[key] === undefined) { if (hookFunctions[key] === undefined) {
@ -1022,7 +1023,7 @@ function getWorkflowHooksIntegrated(
} }
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
} }
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); return new WorkflowHooks(hookFunctions, mode, executionId, workflowData);
} }
/** /**
@ -1064,7 +1065,7 @@ export function getWorkflowHooksWorkerMain(
// TODO: simplifying this for now to just leave the bare minimum hooks // TODO: simplifying this for now to just leave the bare minimum hooks
// const hookFunctions = hookFunctionsPush(); // const hookFunctions = hookFunctionsPush();
// const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); // const preExecuteFunctions = hookFunctionsPreExecute();
// for (const key of Object.keys(preExecuteFunctions)) { // for (const key of Object.keys(preExecuteFunctions)) {
// if (hookFunctions[key] === undefined) { // if (hookFunctions[key] === undefined) {
// hookFunctions[key] = []; // hookFunctions[key] = [];
@ -1105,7 +1106,6 @@ export function getWorkflowHooksWorkerMain(
export function getWorkflowHooksMain( export function getWorkflowHooksMain(
data: IWorkflowExecutionDataProcess, data: IWorkflowExecutionDataProcess,
executionId: string, executionId: string,
isMainProcess = false,
): WorkflowHooks { ): WorkflowHooks {
const hookFunctions = hookFunctionsSave(); const hookFunctions = hookFunctionsSave();
const pushFunctions = hookFunctionsPush(); const pushFunctions = hookFunctionsPush();
@ -1116,14 +1116,12 @@ export function getWorkflowHooksMain(
hookFunctions[key]!.push.apply(hookFunctions[key], pushFunctions[key]); hookFunctions[key]!.push.apply(hookFunctions[key], pushFunctions[key]);
} }
if (isMainProcess) { const preExecuteFunctions = hookFunctionsPreExecute();
const preExecuteFunctions = hookFunctionsPreExecute(); for (const key of Object.keys(preExecuteFunctions)) {
for (const key of Object.keys(preExecuteFunctions)) { if (hookFunctions[key] === undefined) {
if (hookFunctions[key] === undefined) { hookFunctions[key] = [];
hookFunctions[key] = [];
}
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
} }
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
} }
if (!hookFunctions.nodeExecuteBefore) hookFunctions.nodeExecuteBefore = []; if (!hookFunctions.nodeExecuteBefore) hookFunctions.nodeExecuteBefore = [];

View file

@ -158,6 +158,21 @@ export class WorkflowRunner {
): Promise<string> { ): Promise<string> {
// Register a new execution // Register a new execution
const executionId = await this.activeExecutions.add(data, restartExecutionId); const executionId = await this.activeExecutions.add(data, restartExecutionId);
const { id: workflowId, nodes } = data.workflowData;
try {
await this.permissionChecker.check(workflowId, data.userId, nodes);
} catch (error) {
// Create a failed execution with the data for the node, save it and abort execution
const runData = generateFailedExecutionFromError(data.executionMode, error, error.node);
const workflowHooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
await workflowHooks.executeHookFunctions('workflowExecuteBefore', []);
await workflowHooks.executeHookFunctions('workflowExecuteAfter', [runData]);
responsePromise?.reject(error);
this.activeExecutions.remove(executionId);
return executionId;
}
if (responsePromise) { if (responsePromise) {
this.activeExecutions.attachResponsePromise(executionId, responsePromise); this.activeExecutions.attachResponsePromise(executionId, responsePromise);
} }
@ -267,27 +282,7 @@ export class WorkflowRunner {
await this.executionRepository.updateStatus(executionId, 'running'); await this.executionRepository.updateStatus(executionId, 'running');
try { try {
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain( additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
data,
executionId,
true,
);
try {
await this.permissionChecker.check(workflow, data.userId);
} catch (error) {
ErrorReporter.error(error);
// Create a failed execution with the data for the node
// save it and abort execution
const failedExecution = generateFailedExecutionFromError(
data.executionMode,
error,
error.node,
);
await additionalData.hooks.executeHookFunctions('workflowExecuteAfter', [failedExecution]);
this.activeExecutions.remove(executionId, failedExecution);
return;
}
additionalData.hooks.hookFunctions.sendResponse = [ additionalData.hooks.hookFunctions.sendResponse = [
async (response: IExecuteResponsePromiseData): Promise<void> => { async (response: IExecuteResponsePromiseData): Promise<void> => {

View file

@ -4,24 +4,16 @@ import express from 'express';
import http from 'http'; import http from 'http';
import type PCancelable from 'p-cancelable'; import type PCancelable from 'p-cancelable';
import { WorkflowExecute } from 'n8n-core'; import { WorkflowExecute } from 'n8n-core';
import type { import type { ExecutionStatus, IExecuteResponsePromiseData, INodeTypes, IRun } from 'n8n-workflow';
ExecutionError, import { Workflow, sleep, ApplicationError } from 'n8n-workflow';
ExecutionStatus,
IExecuteResponsePromiseData,
INodeTypes,
IRun,
} from 'n8n-workflow';
import { Workflow, NodeOperationError, sleep, ApplicationError } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import config from '@/config'; import config from '@/config';
import type { Job, JobId, JobResponse, WebhookResponse } from '@/Queue'; import type { Job, JobId, JobResponse, WebhookResponse } from '@/Queue';
import { Queue } from '@/Queue'; import { Queue } from '@/Queue';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import { ExecutionRepository } from '@db/repositories/execution.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
@ -180,20 +172,6 @@ export class Worker extends BaseCommand {
}, },
); );
try {
await Container.get(PermissionChecker).check(workflow, workflowOwner.id);
} catch (error) {
if (error instanceof NodeOperationError) {
const failedExecution = generateFailedExecutionFromError(
fullExecutionData.mode,
error,
error.node,
);
await additionalData.hooks.executeHookFunctions('workflowExecuteAfter', [failedExecution]);
}
return { success: true, error: error as ExecutionError };
}
additionalData.hooks.hookFunctions.sendResponse = [ additionalData.hooks.hookFunctions.sendResponse = [
async (response: IExecuteResponsePromiseData): Promise<void> => { async (response: IExecuteResponsePromiseData): Promise<void> => {
const progress: WebhookResponse = { const progress: WebhookResponse = {

View file

@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { WorkflowSettings } from 'n8n-workflow'; import type { INode, WorkflowSettings } from 'n8n-workflow';
import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
@ -87,6 +87,8 @@ beforeAll(async () => {
}); });
describe('check()', () => { describe('check()', () => {
const workflowId = randomPositiveDigit().toString();
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['Workflow', 'Credentials']); await testDb.truncate(['Workflow', 'Credentials']);
}); });
@ -97,56 +99,40 @@ describe('check()', () => {
test('should allow if workflow has no creds', async () => { test('should allow if workflow has no creds', async () => {
const userId = uuid(); const userId = uuid();
const nodes: INode[] = [
{
id: uuid(),
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
parameters: {},
position: [0, 0],
},
];
const workflow = new Workflow({ expect(async () => await permissionChecker.check(workflowId, userId, nodes)).not.toThrow();
id: randomPositiveDigit().toString(),
name: 'test',
active: false,
connections: {},
nodeTypes: mockNodeTypes,
nodes: [
{
id: uuid(),
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
parameters: {},
position: [0, 0],
},
],
});
expect(async () => await permissionChecker.check(workflow, userId)).not.toThrow();
}); });
test('should allow if requesting user is instance owner', async () => { test('should allow if requesting user is instance owner', async () => {
const owner = await createOwner(); const owner = await createOwner();
const nodes: INode[] = [
const workflow = new Workflow({ {
id: randomPositiveDigit().toString(), id: uuid(),
name: 'test', name: 'Action Network',
active: false, type: 'n8n-nodes-base.actionNetwork',
connections: {}, parameters: {},
nodeTypes: mockNodeTypes, typeVersion: 1,
nodes: [ position: [0, 0],
{ credentials: {
id: uuid(), actionNetworkApi: {
name: 'Action Network', id: randomPositiveDigit().toString(),
type: 'n8n-nodes-base.actionNetwork', name: 'Action Network Account',
parameters: {},
typeVersion: 1,
position: [0, 0],
credentials: {
actionNetworkApi: {
id: randomPositiveDigit().toString(),
name: 'Action Network Account',
},
}, },
}, },
], },
}); ];
expect(async () => await permissionChecker.check(workflow, owner.id)).not.toThrow(); expect(async () => await permissionChecker.check(workflowId, owner.id, nodes)).not.toThrow();
}); });
test('should allow if workflow creds are valid subset', async () => { test('should allow if workflow creds are valid subset', async () => {
@ -154,46 +140,38 @@ describe('check()', () => {
const ownerCred = await saveCredential(randomCred(), { user: owner }); const ownerCred = await saveCredential(randomCred(), { user: owner });
const memberCred = await saveCredential(randomCred(), { user: member }); const memberCred = await saveCredential(randomCred(), { user: member });
const nodes: INode[] = [
const workflow = new Workflow({ {
id: randomPositiveDigit().toString(), id: uuid(),
name: 'test', name: 'Action Network',
active: false, type: 'n8n-nodes-base.actionNetwork',
connections: {}, parameters: {},
nodeTypes: mockNodeTypes, typeVersion: 1,
nodes: [ position: [0, 0],
{ credentials: {
id: uuid(), actionNetworkApi: {
name: 'Action Network', id: ownerCred.id,
type: 'n8n-nodes-base.actionNetwork', name: ownerCred.name,
parameters: {},
typeVersion: 1,
position: [0, 0],
credentials: {
actionNetworkApi: {
id: ownerCred.id,
name: ownerCred.name,
},
}, },
}, },
{ },
id: uuid(), {
name: 'Action Network 2', id: uuid(),
type: 'n8n-nodes-base.actionNetwork', name: 'Action Network 2',
parameters: {}, type: 'n8n-nodes-base.actionNetwork',
typeVersion: 1, parameters: {},
position: [0, 0], typeVersion: 1,
credentials: { position: [0, 0],
actionNetworkApi: { credentials: {
id: memberCred.id, actionNetworkApi: {
name: memberCred.name, id: memberCred.id,
}, name: memberCred.name,
}, },
}, },
], },
}); ];
expect(async () => await permissionChecker.check(workflow, owner.id)).not.toThrow(); expect(async () => await permissionChecker.check(workflowId, owner.id, nodes)).not.toThrow();
}); });
test('should deny if workflow creds are not valid subset', async () => { test('should deny if workflow creds are not valid subset', async () => {
@ -247,9 +225,9 @@ describe('check()', () => {
role: 'workflow:owner', role: 'workflow:owner',
}); });
const workflow = new Workflow(workflowDetails); await expect(
permissionChecker.check(workflowDetails.id, member.id, workflowDetails.nodes),
await expect(permissionChecker.check(workflow, member.id)).rejects.toThrow(); ).rejects.toThrow();
}); });
}); });

View file

@ -1,4 +1,4 @@
import { type INodeTypes, Workflow } from 'n8n-workflow'; import type { INode } from 'n8n-workflow';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { UserRepository } from '@db/repositories/user.repository'; import type { UserRepository } from '@db/repositories/user.repository';
@ -21,36 +21,30 @@ describe('PermissionChecker', () => {
license, license,
); );
const workflow = new Workflow({ const workflowId = '1';
id: '1', const nodes: INode[] = [
name: 'test', {
active: false, id: 'node-id',
connections: {}, name: 'HTTP Request',
nodeTypes: mock<INodeTypes>(), type: 'n8n-nodes-base.httpRequest',
nodes: [ parameters: {},
{ typeVersion: 1,
id: 'node-id', position: [0, 0],
name: 'HTTP Request', credentials: {
type: 'n8n-nodes-base.httpRequest', oAuth2Api: {
parameters: {}, id: 'cred-id',
typeVersion: 1, name: 'Custom oAuth2',
position: [0, 0],
credentials: {
oAuth2Api: {
id: 'cred-id',
name: 'Custom oAuth2',
},
}, },
}, },
], },
}); ];
beforeEach(() => jest.clearAllMocks()); beforeEach(() => jest.clearAllMocks());
describe('check', () => { describe('check', () => {
it('should throw if no user is found', async () => { it('should throw if no user is found', async () => {
userRepo.findOneOrFail.mockRejectedValue(new Error('Fail')); userRepo.findOneOrFail.mockRejectedValue(new Error('Fail'));
await expect(permissionChecker.check(workflow, '123')).rejects.toThrow(); await expect(permissionChecker.check(workflowId, '123', nodes)).rejects.toThrow();
expect(license.isSharingEnabled).not.toHaveBeenCalled(); expect(license.isSharingEnabled).not.toHaveBeenCalled();
expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled(); expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled();
expect(sharedCredentialsRepo.getOwnedCredentialIds).not.toHaveBeenCalled(); expect(sharedCredentialsRepo.getOwnedCredentialIds).not.toHaveBeenCalled();
@ -60,7 +54,7 @@ describe('PermissionChecker', () => {
it('should allow a user if they have a global `workflow:execute` scope', async () => { it('should allow a user if they have a global `workflow:execute` scope', async () => {
userRepo.findOneOrFail.mockResolvedValue(user); userRepo.findOneOrFail.mockResolvedValue(user);
user.hasGlobalScope.calledWith('workflow:execute').mockReturnValue(true); user.hasGlobalScope.calledWith('workflow:execute').mockReturnValue(true);
await expect(permissionChecker.check(workflow, user.id)).resolves.not.toThrow(); await expect(permissionChecker.check(workflowId, user.id, nodes)).resolves.not.toThrow();
expect(license.isSharingEnabled).not.toHaveBeenCalled(); expect(license.isSharingEnabled).not.toHaveBeenCalled();
expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled(); expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled();
expect(sharedCredentialsRepo.getOwnedCredentialIds).not.toHaveBeenCalled(); expect(sharedCredentialsRepo.getOwnedCredentialIds).not.toHaveBeenCalled();
@ -77,7 +71,7 @@ describe('PermissionChecker', () => {
it('should validate credential access using only owned credentials', async () => { it('should validate credential access using only owned credentials', async () => {
sharedCredentialsRepo.getOwnedCredentialIds.mockResolvedValue(['cred-id']); sharedCredentialsRepo.getOwnedCredentialIds.mockResolvedValue(['cred-id']);
await expect(permissionChecker.check(workflow, user.id)).resolves.not.toThrow(); await expect(permissionChecker.check(workflowId, user.id, nodes)).resolves.not.toThrow();
expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled(); expect(sharedWorkflowRepo.getSharedUserIds).not.toBeCalled();
expect(sharedCredentialsRepo.getOwnedCredentialIds).toBeCalledWith([user.id]); expect(sharedCredentialsRepo.getOwnedCredentialIds).toBeCalledWith([user.id]);
@ -87,7 +81,7 @@ describe('PermissionChecker', () => {
it('should throw when the user does not have access to the credential', async () => { it('should throw when the user does not have access to the credential', async () => {
sharedCredentialsRepo.getOwnedCredentialIds.mockResolvedValue(['cred-id2']); sharedCredentialsRepo.getOwnedCredentialIds.mockResolvedValue(['cred-id2']);
await expect(permissionChecker.check(workflow, user.id)).rejects.toThrow( await expect(permissionChecker.check(workflowId, user.id, nodes)).rejects.toThrow(
'Node has no access to credential', 'Node has no access to credential',
); );
@ -108,9 +102,9 @@ describe('PermissionChecker', () => {
it('should validate credential access using only owned credentials', async () => { it('should validate credential access using only owned credentials', async () => {
sharedCredentialsRepo.getAccessibleCredentialIds.mockResolvedValue(['cred-id']); sharedCredentialsRepo.getAccessibleCredentialIds.mockResolvedValue(['cred-id']);
await expect(permissionChecker.check(workflow, user.id)).resolves.not.toThrow(); await expect(permissionChecker.check(workflowId, user.id, nodes)).resolves.not.toThrow();
expect(sharedWorkflowRepo.getSharedUserIds).toBeCalledWith(workflow.id); expect(sharedWorkflowRepo.getSharedUserIds).toBeCalledWith(workflowId);
expect(sharedCredentialsRepo.getAccessibleCredentialIds).toBeCalledWith([ expect(sharedCredentialsRepo.getAccessibleCredentialIds).toBeCalledWith([
user.id, user.id,
'another-user', 'another-user',
@ -121,7 +115,7 @@ describe('PermissionChecker', () => {
it('should throw when the user does not have access to the credential', async () => { it('should throw when the user does not have access to the credential', async () => {
sharedCredentialsRepo.getAccessibleCredentialIds.mockResolvedValue(['cred-id2']); sharedCredentialsRepo.getAccessibleCredentialIds.mockResolvedValue(['cred-id2']);
await expect(permissionChecker.check(workflow, user.id)).rejects.toThrow( await expect(permissionChecker.check(workflowId, user.id, nodes)).rejects.toThrow(
'Node has no access to credential', 'Node has no access to credential',
); );

View file

@ -264,7 +264,8 @@ export const pushConnection = defineComponent({
pushData = receivedData.data as IPushDataExecutionFinished; pushData = receivedData.data as IPushDataExecutionFinished;
} }
if (this.workflowsStore.activeExecutionId === pushData.executionId) { const { activeExecutionId } = this.workflowsStore;
if (activeExecutionId === pushData.executionId) {
const activeRunData = const activeRunData =
this.workflowsStore.workflowExecutionData?.data?.resultData?.runData; this.workflowsStore.workflowExecutionData?.data?.resultData?.runData;
if (activeRunData) { if (activeRunData) {
@ -285,7 +286,6 @@ export const pushConnection = defineComponent({
return false; return false;
} }
const { activeExecutionId } = this.workflowsStore;
if (activeExecutionId !== pushData.executionId) { if (activeExecutionId !== pushData.executionId) {
// The workflow which did finish execution did either not get started // The workflow which did finish execution did either not get started
// by this session or we do not have the execution id yet. // by this session or we do not have the execution id yet.
@ -318,7 +318,6 @@ export const pushConnection = defineComponent({
const workflow = this.workflowHelpers.getCurrentWorkflow(); const workflow = this.workflowHelpers.getCurrentWorkflow();
if (runDataExecuted.waitTill !== undefined) { if (runDataExecuted.waitTill !== undefined) {
const activeExecutionId = this.workflowsStore.activeExecutionId;
const workflowSettings = this.workflowsStore.workflowSettings; const workflowSettings = this.workflowsStore.workflowSettings;
const saveManualExecutions = this.rootStore.saveManualExecutions; const saveManualExecutions = this.rootStore.saveManualExecutions;

View file

@ -1191,6 +1191,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
return; return;
} }
this.activeExecutions.unshift(newActiveExecution); this.activeExecutions.unshift(newActiveExecution);
this.activeExecutionId = newActiveExecution.id;
}, },
finishActiveExecution( finishActiveExecution(
finishedActiveExecution: IPushDataExecutionFinished | IPushDataUnsavedExecutionFinished, finishedActiveExecution: IPushDataExecutionFinished | IPushDataUnsavedExecutionFinished,

View file

@ -2083,7 +2083,6 @@ export type WorkflowActivateMode =
| 'leadershipChange'; | 'leadershipChange';
export interface IWorkflowHooksOptionalParameters { export interface IWorkflowHooksOptionalParameters {
parentProcessMode?: string;
retryOf?: string; retryOf?: string;
sessionId?: string; sessionId?: string;
} }

View file

@ -10,16 +10,15 @@ export class CredentialAccessError extends ExecutionBaseError {
constructor( constructor(
readonly node: INode, readonly node: INode,
credentialId: string, credentialId: string,
workflow: { id: string; name?: string }, workflowId: string,
) { ) {
super('Node has no access to credential', { super('Node has no access to credential', {
tags: { tags: {
nodeType: node.type, nodeType: node.type,
}, },
extra: { extra: {
workflowId: workflow.id,
workflowName: workflow.name ?? '',
credentialId, credentialId,
workflowId,
}, },
}); });
} }