mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 21:37:32 -08:00
feat(core): Move execution permission checks earlier in the lifecycle (#8677)
This commit is contained in:
parent
a573146135
commit
059d281fd1
|
@ -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;
|
||||||
},
|
}, {});
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
|
|
@ -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> => {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue