From 59485e75626aec24fe74a32dc1f3242017a94227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 16 Jan 2024 09:58:10 +0000 Subject: [PATCH 1/3] fix: Add custom domains to CORS allow list --- api.planx.uk/server.ts | 10 +++++++++- docker-compose.yml | 3 ++- infrastructure/application/index.ts | 7 ++++--- .../application/utils/generateCORSAllowList.ts | 16 ++++++++++++++++ infrastructure/application/utils/index.ts | 3 +++ infrastructure/common/teams.ts | 5 +++++ 6 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 infrastructure/application/utils/generateCORSAllowList.ts create mode 100644 infrastructure/application/utils/index.ts diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index 0016cd8659..693efcb80c 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -38,11 +38,19 @@ useSwaggerDocs(app); app.set("trust proxy", 1); +const CORS_ALLOWLIST = process.env.CORS_ALLOWLIST?.split(", ") || []; + app.use( cors({ credentials: true, methods: "*", - origin: process.env.EDITOR_URL_EXT, + origin: function (origin, callback) { + if (origin && CORS_ALLOWLIST.includes(origin)) { + callback(null, true); + } else { + callback(new Error("Not allowed by CORS")); + } + }, allowedHeaders: [ "Accept", "Authorization", diff --git a/docker-compose.yml b/docker-compose.yml index 25fc9bccee..f9e380b099 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -141,6 +141,7 @@ services: SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL} ORDNANCE_SURVEY_API_KEY: ${ORDNANCE_SURVEY_API_KEY} MINIO_PORT: ${MINIO_PORT} + CORS_ALLOWLIST: ${EDITOR_URL_EXT} # Local authority config # Lambeth GOV_UK_PAY_TOKEN_LAMBETH: ${GOV_UK_PAY_TOKEN_LAMBETH} @@ -153,7 +154,7 @@ services: UNIFORM_CLIENT_AYLESBURY_VALE: ${UNIFORM_CLIENT_AYLESBURY_VALE} UNIFORM_CLIENT_CHILTERN: ${UNIFORM_CLIENT_CHILTERN} UNIFORM_CLIENT_WYCOMBE: ${UNIFORM_CLIENT_WYCOMBE} - #Medway + # Medway GOV_UK_PAY_TOKEN_MEDWAY: ${GOV_UK_PAY_TOKEN_MEDWAY} sharedb: diff --git a/infrastructure/application/index.ts b/infrastructure/application/index.ts index ca0ffa66e4..799cf57539 100644 --- a/infrastructure/application/index.ts +++ b/infrastructure/application/index.ts @@ -10,9 +10,9 @@ import * as mime from "mime"; import * as tldjs from "tldjs"; import * as url from "url"; -import { generateTeamSecrets } from "./utils/generateTeamSecrets"; +import { generateTeamSecrets, generateCORSAllowList, addRedirectToCloudFlareListenerRule } from "./utils"; import { createHasuraService } from "./services/hasura"; -import { addRedirectToCloudFlareListenerRule } from "./utils/addListenerRule"; +import { CustomDomains } from "../common/teams"; const config = new pulumi.Config(); @@ -25,7 +25,7 @@ const data = new pulumi.StackReference(`planx/data/${env}`); // You can generate tokens here: https://dash.cloudflare.com/profile/api-tokens new pulumi.Config("cloudflare").requireSecret("apiToken"); -const CUSTOM_DOMAINS = +const CUSTOM_DOMAINS: CustomDomains = env === "production" ? [ { @@ -377,6 +377,7 @@ export = async () => { name: "ORDNANCE_SURVEY_API_KEY", value: config.requireSecret("ordnance-survey-api-key"), }, + generateCORSAllowList(CUSTOM_DOMAINS, DOMAIN), ...generateTeamSecrets(config, env), ], }, diff --git a/infrastructure/application/utils/generateCORSAllowList.ts b/infrastructure/application/utils/generateCORSAllowList.ts new file mode 100644 index 0000000000..5700b5ba2c --- /dev/null +++ b/infrastructure/application/utils/generateCORSAllowList.ts @@ -0,0 +1,16 @@ +import * as awsx from "@pulumi/awsx"; + +import { CustomDomains } from "../../common/teams"; + +export const generateCORSAllowList = (customDomains: CustomDomains, domain: string): awsx.ecs.KeyValuePair => { + const customDomainURLs = customDomains.map(team => team.domain); + const editorURL = `https://${domain}`; + const corsAllowList = [...customDomainURLs, editorURL]; + + const secret: awsx.ecs.KeyValuePair = { + name: "CORS_ALLOWLIST", + value: corsAllowList.join(", "), + }; + + return secret; +}; \ No newline at end of file diff --git a/infrastructure/application/utils/index.ts b/infrastructure/application/utils/index.ts new file mode 100644 index 0000000000..6d3e3d95d8 --- /dev/null +++ b/infrastructure/application/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./addListenerRule"; +export * from "./generateCORSAllowList"; +export * from "./generateTeamSecrets"; \ No newline at end of file diff --git a/infrastructure/common/teams.ts b/infrastructure/common/teams.ts index 3a921288ac..c677e1d692 100644 --- a/infrastructure/common/teams.ts +++ b/infrastructure/common/teams.ts @@ -31,4 +31,9 @@ export const teams: Team[] = [ }, ]; +export type CustomDomains = Array<{ + name: string, + domain: string +}>; + export default { teams } \ No newline at end of file From 44e8a7a120061c6e4d2e8c1e96e9046aa5b476ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 16 Jan 2024 10:36:08 +0000 Subject: [PATCH 2/3] fix: Wrap in helper function and handle test envs --- api.planx.uk/server.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index 693efcb80c..a54833d09b 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -3,7 +3,7 @@ import { json, urlencoded } from "body-parser"; import assert from "assert"; import cookieParser from "cookie-parser"; import cookieSession from "cookie-session"; -import cors from "cors"; +import cors, { CorsOptions } from "cors"; import express, { ErrorRequestHandler } from "express"; import noir from "pino-noir"; import pinoLogger from "express-pino-logger"; @@ -38,19 +38,22 @@ useSwaggerDocs(app); app.set("trust proxy", 1); -const CORS_ALLOWLIST = process.env.CORS_ALLOWLIST?.split(", ") || []; +const checkAllowedOrigins: CorsOptions["origin"] = (origin, callback) => { + const isTest = process.env.NODE_ENV === "test"; + const isDevelopment = process.env.APP_ENVIRONMENT === "development"; + const allowList = process.env.CORS_ALLOWLIST?.split(", ") || []; + const isAllowed = origin && allowList.includes(origin); + + isTest || isDevelopment || isAllowed + ? callback(null, true) + : callback(new Error("Not allowed by CORS")); +}; app.use( cors({ credentials: true, methods: "*", - origin: function (origin, callback) { - if (origin && CORS_ALLOWLIST.includes(origin)) { - callback(null, true); - } else { - callback(new Error("Not allowed by CORS")); - } - }, + origin: checkAllowedOrigins, allowedHeaders: [ "Accept", "Authorization", From 183eb8a87f6bce85f7a3a7d55902656c48823463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 16 Jan 2024 11:20:00 +0000 Subject: [PATCH 3/3] fix: Prepend https to team.domain --- api.planx.uk/server.ts | 4 ++-- infrastructure/application/utils/generateCORSAllowList.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index a54833d09b..8c95c29f04 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -42,9 +42,9 @@ const checkAllowedOrigins: CorsOptions["origin"] = (origin, callback) => { const isTest = process.env.NODE_ENV === "test"; const isDevelopment = process.env.APP_ENVIRONMENT === "development"; const allowList = process.env.CORS_ALLOWLIST?.split(", ") || []; - const isAllowed = origin && allowList.includes(origin); + const isAllowed = Boolean(origin && allowList.includes(origin)); - isTest || isDevelopment || isAllowed + !origin || isTest || isDevelopment || isAllowed ? callback(null, true) : callback(new Error("Not allowed by CORS")); }; diff --git a/infrastructure/application/utils/generateCORSAllowList.ts b/infrastructure/application/utils/generateCORSAllowList.ts index 5700b5ba2c..d49aebb77f 100644 --- a/infrastructure/application/utils/generateCORSAllowList.ts +++ b/infrastructure/application/utils/generateCORSAllowList.ts @@ -3,7 +3,7 @@ import * as awsx from "@pulumi/awsx"; import { CustomDomains } from "../../common/teams"; export const generateCORSAllowList = (customDomains: CustomDomains, domain: string): awsx.ecs.KeyValuePair => { - const customDomainURLs = customDomains.map(team => team.domain); + const customDomainURLs = customDomains.map(team => `https://${team.domain}`); const editorURL = `https://${domain}`; const corsAllowList = [...customDomainURLs, editorURL];