feat(core): Add credential runtime checks and prevent tampering in manual run (#4481)
* ✨ Create `PermissionChecker` * ⚡ Adjust helper * 🔥 Remove superseded helpers * ⚡ Use `PermissionChecker` * 🧪 Add test for dynamic router switching * ⚡ Simplify checks * ⚡ Export utils * ⚡ Add missing `init` method * 🧪 Write tests for `PermissionChecker` * 📘 Update types * 🧪 Fix tests * ✨ Set up `runManually()` * ⚡ Refactor to reuse methods * 🧪 Clear shared tables first * 🔀 Adjust merge * ⚡ Adjust imports
This commit is contained in:
@@ -15,6 +15,8 @@ import { LoggerProxy } from 'n8n-workflow';
|
||||
import * as TagHelpers from '@/TagHelpers';
|
||||
import { EECredentialsService as EECredentials } from '../credentials/credentials.service.ee';
|
||||
import { WorkflowsService } from './workflows.services';
|
||||
import { IExecutionPushResponse } from '@/Interfaces';
|
||||
import * as GenericHelpers from '@/GenericHelpers';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const EEWorkflowController = express.Router();
|
||||
@@ -214,9 +216,11 @@ EEWorkflowController.patch(
|
||||
const { tags, ...rest } = req.body;
|
||||
Object.assign(updateData, rest);
|
||||
|
||||
const updatedWorkflow = await EEWorkflows.updateWorkflow(
|
||||
const safeWorkflow = await EEWorkflows.preventTampering(updateData, workflowId, req.user);
|
||||
|
||||
const updatedWorkflow = await WorkflowsService.update(
|
||||
req.user,
|
||||
updateData,
|
||||
safeWorkflow,
|
||||
workflowId,
|
||||
tags,
|
||||
forceSave,
|
||||
@@ -230,3 +234,24 @@ EEWorkflowController.patch(
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* (EE) POST /workflows/run
|
||||
*/
|
||||
EEWorkflowController.post(
|
||||
'/run',
|
||||
ResponseHelper.send(async (req: WorkflowRequest.ManualRun): Promise<IExecutionPushResponse> => {
|
||||
const workflow = new WorkflowEntity();
|
||||
Object.assign(workflow, req.body.workflowData);
|
||||
|
||||
const safeWorkflow = await EEWorkflows.preventTampering(
|
||||
workflow,
|
||||
workflow.id.toString(),
|
||||
req.user,
|
||||
);
|
||||
|
||||
req.body.workflowData.nodes = safeWorkflow.nodes;
|
||||
|
||||
return WorkflowsService.runManually(req.body, req.user, GenericHelpers.getSessionId(req));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import express from 'express';
|
||||
import { INode, LoggerProxy, Workflow } from 'n8n-workflow';
|
||||
import { LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import axios from 'axios';
|
||||
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
|
||||
@@ -10,15 +10,7 @@ import * as GenericHelpers from '@/GenericHelpers';
|
||||
import * as ResponseHelper from '@/ResponseHelper';
|
||||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||
import { whereClause } from '@/CredentialsHelper';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||
import * as TestWebhooks from '@/TestWebhooks';
|
||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||
import {
|
||||
IWorkflowResponse,
|
||||
IExecutionPushResponse,
|
||||
IWorkflowExecutionDataProcess,
|
||||
} from '@/Interfaces';
|
||||
import { IWorkflowResponse, IExecutionPushResponse } from '@/Interfaces';
|
||||
import config from '@/config';
|
||||
import * as TagHelpers from '@/TagHelpers';
|
||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
@@ -260,12 +252,7 @@ workflowsController.patch(
|
||||
const { tags, ...rest } = req.body;
|
||||
Object.assign(updateData, rest);
|
||||
|
||||
const updatedWorkflow = await WorkflowsService.updateWorkflow(
|
||||
req.user,
|
||||
updateData,
|
||||
workflowId,
|
||||
tags,
|
||||
);
|
||||
const updatedWorkflow = await WorkflowsService.update(req.user, updateData, workflowId, tags);
|
||||
|
||||
const { id, ...remainder } = updatedWorkflow;
|
||||
|
||||
@@ -326,82 +313,8 @@ workflowsController.delete(
|
||||
* POST /workflows/run
|
||||
*/
|
||||
workflowsController.post(
|
||||
`/run`,
|
||||
'/run',
|
||||
ResponseHelper.send(async (req: WorkflowRequest.ManualRun): Promise<IExecutionPushResponse> => {
|
||||
const { workflowData } = req.body;
|
||||
const { runData } = req.body;
|
||||
const { pinData } = req.body;
|
||||
const { startNodes } = req.body;
|
||||
const { destinationNode } = req.body;
|
||||
const executionMode = 'manual';
|
||||
const activationMode = 'manual';
|
||||
|
||||
const sessionId = GenericHelpers.getSessionId(req);
|
||||
|
||||
const pinnedTrigger = WorkflowsService.findPinnedTrigger(workflowData, startNodes, pinData);
|
||||
|
||||
// If webhooks nodes exist and are active we have to wait for till we receive a call
|
||||
if (
|
||||
pinnedTrigger === null &&
|
||||
(runData === undefined ||
|
||||
startNodes === undefined ||
|
||||
startNodes.length === 0 ||
|
||||
destinationNode === undefined)
|
||||
) {
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(req.user.id);
|
||||
const nodeTypes = NodeTypes();
|
||||
const workflowInstance = new Workflow({
|
||||
id: workflowData.id?.toString(),
|
||||
name: workflowData.name,
|
||||
nodes: workflowData.nodes,
|
||||
connections: workflowData.connections,
|
||||
active: false,
|
||||
nodeTypes,
|
||||
staticData: undefined,
|
||||
settings: workflowData.settings,
|
||||
});
|
||||
const needsWebhook = await TestWebhooks.getInstance().needsWebhookData(
|
||||
workflowData,
|
||||
workflowInstance,
|
||||
additionalData,
|
||||
executionMode,
|
||||
activationMode,
|
||||
sessionId,
|
||||
destinationNode,
|
||||
);
|
||||
if (needsWebhook) {
|
||||
return {
|
||||
waitingForWebhook: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For manual testing always set to not active
|
||||
workflowData.active = false;
|
||||
|
||||
// Start the workflow
|
||||
const data: IWorkflowExecutionDataProcess = {
|
||||
destinationNode,
|
||||
executionMode,
|
||||
runData,
|
||||
pinData,
|
||||
sessionId,
|
||||
startNodes,
|
||||
workflowData,
|
||||
userId: req.user.id,
|
||||
};
|
||||
|
||||
const hasRunData = (node: INode) => runData !== undefined && !!runData[node.name];
|
||||
|
||||
if (pinnedTrigger && !hasRunData(pinnedTrigger)) {
|
||||
data.startNodes = [pinnedTrigger.name];
|
||||
}
|
||||
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
const executionId = await workflowRunner.run(data);
|
||||
|
||||
return {
|
||||
executionId,
|
||||
};
|
||||
return WorkflowsService.runManually(req.body, req.user, GenericHelpers.getSessionId(req));
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -158,21 +158,17 @@ export class EEWorkflowsService extends WorkflowsService {
|
||||
});
|
||||
}
|
||||
|
||||
static async updateWorkflow(
|
||||
user: User,
|
||||
workflow: WorkflowEntity,
|
||||
workflowId: string,
|
||||
tags?: string[],
|
||||
forceSave?: boolean,
|
||||
): Promise<WorkflowEntity> {
|
||||
static async preventTampering(workflow: WorkflowEntity, workflowId: string, user: User) {
|
||||
const previousVersion = await EEWorkflowsService.get({ id: parseInt(workflowId, 10) });
|
||||
|
||||
if (!previousVersion) {
|
||||
throw new ResponseHelper.ResponseError('Workflow not found', undefined, 404);
|
||||
}
|
||||
|
||||
const allCredentials = await EECredentials.getAll(user);
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||
workflow = WorkflowHelpers.validateWorkflowCredentialUsage(
|
||||
return WorkflowHelpers.validateWorkflowCredentialUsage(
|
||||
workflow,
|
||||
previousVersion,
|
||||
allCredentials,
|
||||
@@ -184,7 +180,5 @@ export class EEWorkflowsService extends WorkflowsService {
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
return super.updateWorkflow(user, workflow, workflowId, tags, forceSave);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IPinData, JsonObject, jsonParse, LoggerProxy } from 'n8n-workflow';
|
||||
import { FindManyOptions, FindOneOptions, In, ObjectLiteral } from 'typeorm';
|
||||
import { validate as jsonSchemaValidate } from 'jsonschema';
|
||||
import { INode, IPinData, JsonObject, jsonParse, LoggerProxy, Workflow } from 'n8n-workflow';
|
||||
import { FindManyOptions, FindOneOptions, In, ObjectLiteral } from 'typeorm';
|
||||
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
|
||||
import * as Db from '@/Db';
|
||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||
@@ -14,8 +14,13 @@ import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { externalHooks } from '@/Server';
|
||||
import * as TagHelpers from '@/TagHelpers';
|
||||
import { WorkflowRequest } from '@/requests';
|
||||
import { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
|
||||
import { NodeTypes } from '@/NodeTypes';
|
||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||
import * as TestWebhooks from '@/TestWebhooks';
|
||||
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
||||
import { IWorkflowDb } from '..';
|
||||
|
||||
export interface IGetWorkflowsQueryFilter {
|
||||
id?: number | string;
|
||||
@@ -162,7 +167,7 @@ export class WorkflowsService {
|
||||
});
|
||||
}
|
||||
|
||||
static async updateWorkflow(
|
||||
static async update(
|
||||
user: User,
|
||||
workflow: WorkflowEntity,
|
||||
workflowId: string,
|
||||
@@ -308,4 +313,86 @@ export class WorkflowsService {
|
||||
|
||||
return updatedWorkflow;
|
||||
}
|
||||
|
||||
static async runManually(
|
||||
{
|
||||
workflowData,
|
||||
runData,
|
||||
pinData,
|
||||
startNodes,
|
||||
destinationNode,
|
||||
}: WorkflowRequest.ManualRunPayload,
|
||||
user: User,
|
||||
sessionId?: string,
|
||||
) {
|
||||
const EXECUTION_MODE = 'manual';
|
||||
const ACTIVATION_MODE = 'manual';
|
||||
|
||||
const pinnedTrigger = WorkflowsService.findPinnedTrigger(workflowData, startNodes, pinData);
|
||||
|
||||
// If webhooks nodes exist and are active we have to wait for till we receive a call
|
||||
if (
|
||||
pinnedTrigger === null &&
|
||||
(runData === undefined ||
|
||||
startNodes === undefined ||
|
||||
startNodes.length === 0 ||
|
||||
destinationNode === undefined)
|
||||
) {
|
||||
const workflow = new Workflow({
|
||||
id: workflowData.id?.toString(),
|
||||
name: workflowData.name,
|
||||
nodes: workflowData.nodes,
|
||||
connections: workflowData.connections,
|
||||
active: false,
|
||||
nodeTypes: NodeTypes(),
|
||||
staticData: undefined,
|
||||
settings: workflowData.settings,
|
||||
});
|
||||
|
||||
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
|
||||
|
||||
const needsWebhook = await TestWebhooks.getInstance().needsWebhookData(
|
||||
workflowData,
|
||||
workflow,
|
||||
additionalData,
|
||||
EXECUTION_MODE,
|
||||
ACTIVATION_MODE,
|
||||
sessionId,
|
||||
destinationNode,
|
||||
);
|
||||
if (needsWebhook) {
|
||||
return {
|
||||
waitingForWebhook: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For manual testing always set to not active
|
||||
workflowData.active = false;
|
||||
|
||||
// Start the workflow
|
||||
const data: IWorkflowExecutionDataProcess = {
|
||||
destinationNode,
|
||||
executionMode: EXECUTION_MODE,
|
||||
runData,
|
||||
pinData,
|
||||
sessionId,
|
||||
startNodes,
|
||||
workflowData,
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
const hasRunData = (node: INode) => runData !== undefined && !!runData[node.name];
|
||||
|
||||
if (pinnedTrigger && !hasRunData(pinnedTrigger)) {
|
||||
data.startNodes = [pinnedTrigger.name];
|
||||
}
|
||||
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
const executionId = await workflowRunner.run(data);
|
||||
|
||||
return {
|
||||
executionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user