From c0187a9c557aa6a973b87c411b062f8ff8ede2a6 Mon Sep 17 00:00:00 2001 From: Michael Kret <michael.k@radency.com> Date: Tue, 17 Oct 2023 16:26:45 +0300 Subject: [PATCH] :zap: add access control options for Webhook --- packages/cli/src/Interfaces.ts | 4 ++ packages/cli/src/TestWebhooks.ts | 28 ++++++++++ packages/cli/src/WebhookHelpers.ts | 19 +++++++ .../nodes-base/nodes/Webhook/Webhook.node.ts | 1 + .../nodes-base/nodes/Webhook/description.ts | 54 +++++++++++++++++++ 5 files changed, 106 insertions(+) diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index b47b1a91c7fc61..4e943a767be2d2 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -269,6 +269,10 @@ export type WaitingWebhookRequest = WebhookRequest & { export interface IWebhookManager { getWebhookMethods?: (path: string) => Promise<IHttpRequestMethods[]>; + getAccessControlOptions?: ( + path: string, + httpMethod: IHttpRequestMethods, + ) => Promise<IDataObject | null>; executeWebhook(req: WebhookRequest, res: Response): Promise<IResponseCallbackData>; } diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index 4cd5d99809e180..53b277b94fdb56 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -8,6 +8,7 @@ import type { Workflow, WorkflowActivateMode, WorkflowExecuteMode, + IDataObject, } from 'n8n-workflow'; import { ActiveWebhooks } from '@/ActiveWebhooks'; @@ -177,6 +178,33 @@ export class TestWebhooks implements IWebhookManager { return webhookMethods; } + /** + * Gets all request methods associated with a single test webhook + */ + async getAccessControlOptions(path: string, httpMethod: string): Promise<IDataObject | null> { + const webhookWorkflow = Object.keys(this.testWebhookData).find( + (key) => key.includes(path) && key.startsWith(httpMethod), + ); + + const nodes = webhookWorkflow ? this.testWebhookData[webhookWorkflow].workflow.nodes : {}; + + if (!Object.keys(nodes).length) { + return null; + } + + const result = Object.values(nodes).find((node) => { + return ( + node.type === 'n8n-nodes-base.webhook' && + node.parameters?.path === path && + node.parameters?.httpMethod === httpMethod + ); + }); + + const { accessControl } = (result?.parameters?.options as IDataObject) || {}; + + return accessControl ? ((accessControl as IDataObject).values as IDataObject) : null; + } + /** * Checks if it has to wait for webhook data to execute the workflow. * If yes it waits for it and resolves with the result of the workflow if not it simply resolves with undefined diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 6a200e694f37a5..2dce404729666d 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -102,6 +102,25 @@ export const webhookRequestHandler = } if (method === 'OPTIONS') { + const controlRequestMethod = req.headers['access-control-request-method']; + + if (webhookManager.getAccessControlOptions && controlRequestMethod) { + try { + const accessControlOptions = await webhookManager.getAccessControlOptions( + path, + controlRequestMethod as IHttpRequestMethods, + ); + + if (accessControlOptions) { + const { allowMethods, allowOrigin, maxAge } = accessControlOptions; + + res.header('Access-Control-Allow-Methods', (allowMethods as string) || '*'); + res.header('Access-Control-Allow-Origin', (allowOrigin as string) || '*'); + res.header('Access-Control-Max-Age', String((maxAge as number) * 1000) || '60000'); + } + } catch (error) {} + } + return ResponseHelper.sendSuccessResponse(res, {}, true, 204); } diff --git a/packages/nodes-base/nodes/Webhook/Webhook.node.ts b/packages/nodes-base/nodes/Webhook/Webhook.node.ts index 191d2af7f5a598..b6eec3ee361fad 100644 --- a/packages/nodes-base/nodes/Webhook/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook/Webhook.node.ts @@ -99,6 +99,7 @@ export class Webhook extends Node { ignoreBots: boolean; rawBody: Buffer; responseData?: string; + accessControl: IDataObject; }; const req = context.getRequestObject(); const resp = context.getResponseObject(); diff --git a/packages/nodes-base/nodes/Webhook/description.ts b/packages/nodes-base/nodes/Webhook/description.ts index 33be6f5ae98953..f475cad0fb9355 100644 --- a/packages/nodes-base/nodes/Webhook/description.ts +++ b/packages/nodes-base/nodes/Webhook/description.ts @@ -335,5 +335,59 @@ export const optionsProperty: INodeProperties = { default: 'data', description: 'Name of the property to return the data of instead of the whole JSON', }, + { + displayName: 'Access Control', + name: 'accessControl', + placeholder: 'Add Options Header', + description: 'Add headers to the preflight response', + type: 'fixedCollection', + default: { + values: { + allowOrigin: '*', + allowMethods: '*', + // allowHeaders: '', + maxAge: 60, + }, + }, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Origin', + name: 'allowOrigin', + type: 'string', + default: '', + description: 'The origin(s) to allow requests from', + }, + { + displayName: 'Methods', + name: 'allowMethods', + type: 'string', + default: '', + description: 'The methods to allow', + }, + // { + // displayName: 'Headers', + // name: 'allowHeaders', + // type: 'string', + // default: '', + // description: 'The headers to allow', + // }, + { + displayName: 'Max Age', + name: 'maxAge', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'The max age to allow', + }, + ], + }, + ], + }, ], };