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:
Iván Ovejero
2022-11-11 11:14:45 +01:00
committed by GitHub
parent 50f7538779
commit d35d63a855
16 changed files with 497 additions and 233 deletions

View File

@@ -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));
}),
);

View File

@@ -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));
}),
);

View File

@@ -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);
}
}

View File

@@ -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,
};
}
}