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',
+						},
+					],
+				},
+			],
+		},
 	],
 };