From f34f6529b5375608f248aecaa80a0d807cca1de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Sun, 3 Sep 2023 12:30:53 +0100 Subject: [PATCH] feat: Implement useRoleAuth in API, update tests --- .../feedback/downloadFeedbackCSV.test.ts | 16 ++++++-- api.planx.uk/admin/session/bops.test.ts | 9 ++++- api.planx.uk/admin/session/csv.test.ts | 12 +++++- api.planx.uk/admin/session/html.test.ts | 9 ++++- api.planx.uk/admin/session/oneAppXML.test.ts | 12 +++++- api.planx.uk/admin/session/summary.test.ts | 9 ++++- api.planx.uk/admin/session/zip.test.ts | 9 ++++- api.planx.uk/docs/index.ts | 2 +- api.planx.uk/editor/copyFlow.test.ts | 21 ++++++++-- api.planx.uk/editor/copyFlow.ts | 6 +-- api.planx.uk/editor/copyPortalAsFlow.test.ts | 15 ++++++- api.planx.uk/editor/copyPortalAsFlow.ts | 4 -- api.planx.uk/editor/findReplace.test.ts | 21 ++++++++-- api.planx.uk/editor/findReplace.ts | 3 -- api.planx.uk/editor/moveFlow.test.ts | 9 ++++- api.planx.uk/editor/moveFlow.ts | 4 -- api.planx.uk/editor/publish.test.ts | 31 ++++++++++----- api.planx.uk/editor/publish.ts | 8 +--- api.planx.uk/modules/auth/middleware.ts | 4 +- api.planx.uk/modules/team/index.test.ts | 39 ++++++++++++------- api.planx.uk/modules/team/routes.ts | 2 +- api.planx.uk/server.ts | 31 ++++++++------- api.planx.uk/tests/mockJWT.js | 20 ---------- api.planx.uk/tests/mockJWT.ts | 21 ++++++++++ 24 files changed, 211 insertions(+), 106 deletions(-) delete mode 100644 api.planx.uk/tests/mockJWT.js create mode 100644 api.planx.uk/tests/mockJWT.ts diff --git a/api.planx.uk/admin/feedback/downloadFeedbackCSV.test.ts b/api.planx.uk/admin/feedback/downloadFeedbackCSV.test.ts index 933fb863a6..82d010aad4 100644 --- a/api.planx.uk/admin/feedback/downloadFeedbackCSV.test.ts +++ b/api.planx.uk/admin/feedback/downloadFeedbackCSV.test.ts @@ -48,6 +48,7 @@ const mockFeedback: Feedback = { describe("Download feedback CSV endpoint", () => { afterEach(() => jest.clearAllMocks()); + const auth = authHeader({ role: "platformAdmin" }); it("requires a user to be logged in", async () => { await supertest(app) @@ -60,10 +61,17 @@ describe("Download feedback CSV endpoint", () => { ); }); + it("requires a user to have the platform admin role", async () => { + await supertest(app) + .get(ENDPOINT) + .set(authHeader({ role: "teamEditor" })) + .expect(403); + }); + it("requires the 'cookie' query parameter", async () => { await supertest(app) .get(ENDPOINT) - .set(authHeader()) + .set(auth) .expect(401) .then((res) => expect(res.body).toEqual({ error: "Missing cookie" })); }); @@ -72,7 +80,7 @@ describe("Download feedback CSV endpoint", () => { mockAxios.post.mockRejectedValue(new Error("FeedbackFish query failed!")); await supertest(app) .get(ENDPOINT) - .set(authHeader()) + .set(auth) .query({ cookie: "test cookie" }) .expect(500) .then((res) => @@ -87,7 +95,7 @@ describe("Download feedback CSV endpoint", () => { const cookie = "test cookie"; await supertest(app) .get(ENDPOINT) - .set(authHeader()) + .set(auth) .query({ cookie }) .expect(200) .then(() => { @@ -106,7 +114,7 @@ describe("Download feedback CSV endpoint", () => { const cookie = "test cookie"; await supertest(app) .get(ENDPOINT) - .set(authHeader()) + .set(auth) .query({ cookie }) .expect(200) .expect("content-type", "text/csv; charset=utf-8"); diff --git a/api.planx.uk/admin/session/bops.test.ts b/api.planx.uk/admin/session/bops.test.ts index bdbf27c896..825ff8ce6e 100644 --- a/api.planx.uk/admin/session/bops.test.ts +++ b/api.planx.uk/admin/session/bops.test.ts @@ -32,10 +32,17 @@ describe("BOPS payload admin endpoint", () => { ); }); + it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .get(endpoint`123`) + .set(authHeader({ role: "teamEditor" })) + .expect(403); + }); + it("returns a JSON payload", async () => { await supertest(app) .get(endpoint`123`) - .set(authHeader()) + .set(authHeader({ role: "platformAdmin" })) .expect(200) .expect("content-type", "application/json; charset=utf-8") .then((res) => expect(res.body).toEqual(expectedPayload)); diff --git a/api.planx.uk/admin/session/csv.test.ts b/api.planx.uk/admin/session/csv.test.ts index c7f92ede8b..a493fb410d 100644 --- a/api.planx.uk/admin/session/csv.test.ts +++ b/api.planx.uk/admin/session/csv.test.ts @@ -27,6 +27,7 @@ jest.mock("@opensystemslab/planx-core", () => { describe("CSV data admin endpoint", () => { afterEach(() => jest.clearAllMocks()); + const auth = authHeader({ role: "platformAdmin" }); it("requires a user to be logged in", async () => { await supertest(app) @@ -39,10 +40,17 @@ describe("CSV data admin endpoint", () => { ); }); + it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .get(endpoint`123`) + .set(authHeader({ role: "teamEditor" })) + .expect(403); + }); + it("returns a CSV-formatted payload", async () => { await supertest(app) .get(endpoint`123`) - .set(authHeader()) + .set(auth) .expect(200) .expect("content-type", "application/json; charset=utf-8") .then((res) => @@ -59,7 +67,7 @@ describe("CSV data admin endpoint", () => { it("downloads a CSV file if a query parameter is passed", async () => { await supertest(app) .get(endpoint`123` + `?download=true`) - .set(authHeader()) + .set(auth) .expect(200) .expect("content-type", "text/csv; charset=utf-8"); }); diff --git a/api.planx.uk/admin/session/html.test.ts b/api.planx.uk/admin/session/html.test.ts index 5ee4f405c8..f6fdf385e2 100644 --- a/api.planx.uk/admin/session/html.test.ts +++ b/api.planx.uk/admin/session/html.test.ts @@ -39,10 +39,17 @@ describe("HTML data admin endpoint", () => { ); }); + it("requires a user to have the 'platformAdmin' role", () => { + return supertest(app) + .get(endpoint`123`) + .set(authHeader({ role: "teamViewer" })) + .expect(403); + }); + it.skip("returns a HTML-formatted payload", () => { return supertest(app) .get(endpoint`123`) - .set(authHeader()) + .set(authHeader({ role: "platformAdmin" })) .expect(200) .expect("content-type", "text/html; charset=utf-8") .then((res) => diff --git a/api.planx.uk/admin/session/oneAppXML.test.ts b/api.planx.uk/admin/session/oneAppXML.test.ts index 46f3c9baba..7b05e6a7d2 100644 --- a/api.planx.uk/admin/session/oneAppXML.test.ts +++ b/api.planx.uk/admin/session/oneAppXML.test.ts @@ -30,6 +30,7 @@ describe("OneApp XML endpoint", () => { }); afterEach(() => jest.clearAllMocks()); + const auth = authHeader({ role: "platformAdmin" }); it("requires a user to be logged in", async () => { await supertest(app) @@ -42,10 +43,17 @@ describe("OneApp XML endpoint", () => { ); }); + it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .get(endpoint`abc123`) + .set(authHeader({ role: "teamEditor" })) + .expect(403); + }); + it("returns an error if sessionID is invalid", async () => { await supertest(app) .get(endpoint`xyz789`) - .set(authHeader()) + .set(auth) .expect(500) .then((res) => expect(res.body.error).toMatch(/Invalid sessionID/)); }); @@ -53,7 +61,7 @@ describe("OneApp XML endpoint", () => { it("returns XML", async () => { await supertest(app) .get(endpoint`abc123`) - .set(authHeader()) + .set(auth) .expect(200) .expect("content-type", "text/xml; charset=utf-8") .then((res) => { diff --git a/api.planx.uk/admin/session/summary.test.ts b/api.planx.uk/admin/session/summary.test.ts index e27678ff84..fd35686a41 100644 --- a/api.planx.uk/admin/session/summary.test.ts +++ b/api.planx.uk/admin/session/summary.test.ts @@ -32,10 +32,17 @@ describe("Session summary admin endpoint", () => { ); }); + it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .get(endpoint`abc123`) + .set(authHeader({ role: "teamEditor" })) + .expect(403); + }); + it("returns JSON", async () => { await supertest(app) .get(endpoint`abc123`) - .set(authHeader()) + .set(authHeader({ role: "platformAdmin" })) .expect(200) .then((res) => { expect(res.text).toBe("{}"); diff --git a/api.planx.uk/admin/session/zip.test.ts b/api.planx.uk/admin/session/zip.test.ts index d09479a150..9e7e6d5979 100644 --- a/api.planx.uk/admin/session/zip.test.ts +++ b/api.planx.uk/admin/session/zip.test.ts @@ -26,10 +26,17 @@ describe("zip data admin endpoint", () => { ); }); + it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .get(endpoint`123`) + .set(authHeader({ role: "teamEditor" })) + .expect(403); + }); + it("downloads a zip file", async () => { await supertest(app) .get(endpoint`123`) - .set(authHeader()) + .set(authHeader({ role: "platformAdmin" })) .expect(200) .expect("content-type", "application/zip"); }); diff --git a/api.planx.uk/docs/index.ts b/api.planx.uk/docs/index.ts index caf2462ff1..7ecc2744c1 100644 --- a/api.planx.uk/docs/index.ts +++ b/api.planx.uk/docs/index.ts @@ -66,7 +66,7 @@ const responses = { properties: { error: { type: "string", - enum: ["No authorization token was found"], + enum: ["Access denied"], }, }, }, diff --git a/api.planx.uk/editor/copyFlow.test.ts b/api.planx.uk/editor/copyFlow.test.ts index 641421c718..7cf73cb670 100644 --- a/api.planx.uk/editor/copyFlow.test.ts +++ b/api.planx.uk/editor/copyFlow.test.ts @@ -37,6 +37,8 @@ beforeEach(() => { }); }); +const auth = authHeader({ role: "teamEditor" }); + it("returns an error if authorization headers are not set", async () => { const validBody = { insert: false, @@ -54,6 +56,19 @@ it("returns an error if authorization headers are not set", async () => { }); }); +it("returns an error if the user does not have the correct role", async () => { + const validBody = { + insert: false, + replaceValue: "T3ST", + }; + + await supertest(app) + .post("/flows/1/copy") + .send(validBody) + .set(authHeader({ role: "teamViewer" })) + .expect(403); +}); + it("returns an error if required replacement characters are not provided in the request body", async () => { const invalidBody = { insert: false, @@ -62,7 +77,7 @@ it("returns an error if required replacement characters are not provided in the await supertest(app) .post("/flows/1/copy") .send(invalidBody) - .set(authHeader()) + .set(auth) .expect(400) .then((res) => { expect(res.body).toEqual({ @@ -80,7 +95,7 @@ it("returns copied unique flow data without inserting a new record", async () => await supertest(app) .post("/flows/1/copy") .send(body) - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual(mockCopyFlowResponse); @@ -96,7 +111,7 @@ it("inserts copied unique flow data", async () => { await supertest(app) .post("/flows/1/copy") .send(body) - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual(mockCopyFlowResponseInserted); diff --git a/api.planx.uk/editor/copyFlow.ts b/api.planx.uk/editor/copyFlow.ts index 44af337758..2549625143 100644 --- a/api.planx.uk/editor/copyFlow.ts +++ b/api.planx.uk/editor/copyFlow.ts @@ -8,10 +8,6 @@ const copyFlow = async ( next: NextFunction, ): Promise => { try { - if (!req.user?.sub) { - return next({ status: 401, message: "User ID missing from JWT" }); - } - if (!req.params?.flowId || !req.body?.replaceValue) { return next({ status: 400, @@ -29,7 +25,7 @@ const copyFlow = async ( const shouldInsert = (req.body?.insert as boolean) || false; if (shouldInsert) { const newSlug = flow.slug + "-copy"; - const creatorId = parseInt(req.user.sub, 10); + const creatorId = parseInt(req.user!.sub!, 10); // Insert the flow and an associated operation await insertFlow( flow.team_id, diff --git a/api.planx.uk/editor/copyPortalAsFlow.test.ts b/api.planx.uk/editor/copyPortalAsFlow.test.ts index 4956162478..46b84d0268 100644 --- a/api.planx.uk/editor/copyPortalAsFlow.test.ts +++ b/api.planx.uk/editor/copyPortalAsFlow.test.ts @@ -17,10 +17,21 @@ beforeEach(() => { }); }); +it("requires a user to be logged in", async () => { + await supertest(app).get("/flows/1/copy-portal/eyOm0NyDSl").expect(401); +}); + +it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .get("/flows/1/copy-portal/eyOm0NyDSl") + .set(authHeader({ role: "teamEditor" })) + .expect(403); +}); + it("throws an error if the portalNodeId parameter is not a portal (type = 300)", async () => { await supertest(app) .get("/flows/1/copy-portal/eyOm0NyDSl") - .set(authHeader()) + .set(authHeader({ role: "platformAdmin" })) .expect(404) .then((res) => { expect(res.body).toEqual({ @@ -32,7 +43,7 @@ it("throws an error if the portalNodeId parameter is not a portal (type = 300)", it("returns transformed, unique flow data for a valid internal portal", async () => { await supertest(app) .get("/flows/1/copy-portal/MgCe3pSTrt") - .set(authHeader()) + .set(authHeader({ role: "platformAdmin" })) .expect(200) .then((res) => { // the portalNodeId param should have been overwritten as _root diff --git a/api.planx.uk/editor/copyPortalAsFlow.ts b/api.planx.uk/editor/copyPortalAsFlow.ts index c38e7b7969..c220942123 100644 --- a/api.planx.uk/editor/copyPortalAsFlow.ts +++ b/api.planx.uk/editor/copyPortalAsFlow.ts @@ -11,10 +11,6 @@ const copyPortalAsFlow = async ( next: NextFunction, ) => { try { - if (!req.user?.sub) { - return next({ status: 401, message: "User ID missing from JWT" }); - } - // fetch the parent flow data const flow = await getFlowData(req.params.flowId); if (!flow) { diff --git a/api.planx.uk/editor/findReplace.test.ts b/api.planx.uk/editor/findReplace.test.ts index fd2f43e05f..8678d8e476 100644 --- a/api.planx.uk/editor/findReplace.test.ts +++ b/api.planx.uk/editor/findReplace.test.ts @@ -29,10 +29,23 @@ beforeEach(() => { }); }); +const auth = authHeader({ role: "platformAdmin" }); + +it("requires a user to be logged in", async () => { + await supertest(app).post("/flows/1/search").expect(401); +}); + +it("requires a user to have the 'platformAdmin' role", async () => { + await supertest(app) + .post("/flows/1/search") + .set(authHeader({ role: "teamEditor" })) + .expect(403); +}); + it("throws an error if missing query parameter `find`", async () => { await supertest(app) .post("/flows/1/search") - .set(authHeader()) + .set(auth) .expect(401) .then((res) => { expect(res.body).toEqual({ @@ -44,7 +57,7 @@ it("throws an error if missing query parameter `find`", async () => { it("finds matches", async () => { await supertest(app) .post("/flows/1/search?find=designated.monument") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -68,7 +81,7 @@ it("finds matches", async () => { it("does not replace if no matches are found", async () => { await supertest(app) .post("/flows/1/search?find=bananas&replace=monument") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -80,7 +93,7 @@ it("does not replace if no matches are found", async () => { it("updates flow data and returns matches if there are matches", async () => { await supertest(app) .post("/flows/1/search?find=designated.monument&replace=monument") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual({ diff --git a/api.planx.uk/editor/findReplace.ts b/api.planx.uk/editor/findReplace.ts index 78a133ca2c..5e5a2a0f93 100644 --- a/api.planx.uk/editor/findReplace.ts +++ b/api.planx.uk/editor/findReplace.ts @@ -104,9 +104,6 @@ const findAndReplaceInFlow = async ( next: NextFunction, ): Promise => { try { - if (!req.user?.sub) - return next({ status: 401, message: "User ID missing from JWT" }); - const flow = await getFlowData(req.params.flowId); if (!flow) return next({ status: 401, message: "Unknown flowId" }); diff --git a/api.planx.uk/editor/moveFlow.test.ts b/api.planx.uk/editor/moveFlow.test.ts index 51c413deef..f4be83a537 100644 --- a/api.planx.uk/editor/moveFlow.test.ts +++ b/api.planx.uk/editor/moveFlow.test.ts @@ -44,10 +44,17 @@ it("returns an error if authorization headers are not set", async () => { }); }); +it("returns an error if the user does not have the 'teamEditor' role", async () => { + await supertest(app) + .post("/flows/1/move/new-team") + .set(authHeader({ role: "teamViewer" })) + .expect(403); +}); + it("moves a flow to a new team", async () => { await supertest(app) .post("/flows/1/move/new-team") - .set(authHeader()) + .set(authHeader({ role: "teamEditor" })) .expect(200) .then((res) => { expect(res.body).toEqual({ diff --git a/api.planx.uk/editor/moveFlow.ts b/api.planx.uk/editor/moveFlow.ts index 622b47a5c1..add5717705 100644 --- a/api.planx.uk/editor/moveFlow.ts +++ b/api.planx.uk/editor/moveFlow.ts @@ -9,10 +9,6 @@ const moveFlow = async ( next: NextFunction, ): Promise => { try { - if (!req.user?.sub) { - return next({ status: 401, message: "User ID missing from JWT" }); - } - if (!req.params?.flowId || !req.params?.teamSlug) { return next({ status: 400, diff --git a/api.planx.uk/editor/publish.test.ts b/api.planx.uk/editor/publish.test.ts index c6c909239a..504ac123f5 100644 --- a/api.planx.uk/editor/publish.test.ts +++ b/api.planx.uk/editor/publish.test.ts @@ -42,11 +42,24 @@ beforeEach(() => { }); }); +const auth = authHeader({ role: "platformAdmin" }); + +it("requires a user to be logged in", async () => { + await supertest(app).post("/flows/1/publish").expect(401); +}); + +it("requires a user to have the 'teamEditor' role", async () => { + await supertest(app) + .post("/flows/1/publish") + .set(authHeader({ role: "teamViewer" })) + .expect(403); +}); + describe("publish", () => { it("does not update if there are no new changes", async () => { await supertest(app) .post("/flows/1/publish") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -94,7 +107,7 @@ describe("publish", () => { await supertest(app) .post("/flows/1/publish") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -142,7 +155,7 @@ describe("sections validation on diff", () => { await supertest(app) .post("/flows/1/diff") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -177,7 +190,7 @@ describe("sections validation on diff", () => { await supertest(app) .post("/flows/1/diff") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body).toEqual({ @@ -209,7 +222,7 @@ describe("invite to pay validation on diff", () => { await supertest(app) .post("/flows/1/diff") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body.message).toEqual("Cannot publish an invalid flow"); @@ -242,7 +255,7 @@ describe("invite to pay validation on diff", () => { await supertest(app) .post("/flows/1/diff") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body.message).toEqual("Cannot publish an invalid flow"); @@ -270,7 +283,7 @@ describe("invite to pay validation on diff", () => { await supertest(app) .post("/flows/1/diff") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body.message).toEqual("Cannot publish an invalid flow"); @@ -301,7 +314,7 @@ describe("invite to pay validation on diff", () => { await supertest(app) .post("/flows/1/diff") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body.message).toEqual("Cannot publish an invalid flow"); @@ -334,7 +347,7 @@ describe("invite to pay validation on diff", () => { await supertest(app) .post("/flows/1/diff") - .set(authHeader()) + .set(auth) .expect(200) .then((res) => { expect(res.body.message).toEqual("Cannot publish an invalid flow"); diff --git a/api.planx.uk/editor/publish.ts b/api.planx.uk/editor/publish.ts index f25cc92d27..ed19f40ba1 100644 --- a/api.planx.uk/editor/publish.ts +++ b/api.planx.uk/editor/publish.ts @@ -11,9 +11,6 @@ const validateAndDiffFlow = async ( res: Response, next: NextFunction, ): Promise => { - if (!req.user?.sub) - return next({ status: 401, message: "User ID missing from JWT" }); - try { const flattenedFlow = await dataMerged(req.params.flowId); @@ -71,9 +68,6 @@ const publishFlow = async ( res: Response, next: NextFunction, ): Promise => { - if (!req.user?.sub) - return next({ status: 401, message: "User ID missing from JWT" }); - try { const flattenedFlow = await dataMerged(req.params.flowId); const mostRecent = await getMostRecentPublishedFlow(req.params.flowId); @@ -107,7 +101,7 @@ const publishFlow = async ( { data: flattenedFlow, flow_id: req.params.flowId, - publisher_id: parseInt(req.user.sub, 10), + publisher_id: parseInt(req.user!.sub!, 10), summary: req.query?.summary || null, }, ); diff --git a/api.planx.uk/modules/auth/middleware.ts b/api.planx.uk/modules/auth/middleware.ts index 65e934bab2..ee11d02c86 100644 --- a/api.planx.uk/modules/auth/middleware.ts +++ b/api.planx.uk/modules/auth/middleware.ts @@ -126,7 +126,7 @@ export const useRoleAuth: UseRoleAuth = if (!req?.user) return next({ status: 401, - message: "User details missing from request", + message: "No authorization token was found", }); const userRoles = @@ -134,7 +134,7 @@ export const useRoleAuth: UseRoleAuth = if (!userRoles) return next({ status: 401, - message: "User roles missing from request", + message: "User roles missing from token", }); const userId = req.user.sub; diff --git a/api.planx.uk/modules/team/index.test.ts b/api.planx.uk/modules/team/index.test.ts index 00bab61920..0abd2465d2 100644 --- a/api.planx.uk/modules/team/index.test.ts +++ b/api.planx.uk/modules/team/index.test.ts @@ -18,6 +18,8 @@ jest.mock("@opensystemslab/planx-core", () => { }; }); +const auth = authHeader({ role: "platformAdmin" }); + describe("Adding a user to a team", () => { it("requires authentication", async () => { await supertest(app) @@ -29,10 +31,21 @@ describe("Adding a user to a team", () => { .expect(401); }); + it("requires the 'platformAdmin' role", async () => { + await supertest(app) + .put("/team/123/add-member") + .set(authHeader({ role: "teamEditor" })) + .send({ + userId: 123, + role: "teamViewer", + }) + .expect(403); + }); + it("validates that userId is required", async () => { await supertest(app) .put("/team/123/add-member") - .set(authHeader()) + .set(auth) .send({ role: "teamViewer", }) @@ -46,7 +59,7 @@ describe("Adding a user to a team", () => { it("validates that role is required", async () => { await supertest(app) .put("/team/123/add-member") - .set(authHeader()) + .set(auth) .send({ userId: 123, }) @@ -60,7 +73,7 @@ describe("Adding a user to a team", () => { it("validates that role must one an accepted value", async () => { await supertest(app) .put("/team/123/add-member") - .set(authHeader()) + .set(auth) .send({ userId: 123, role: "pirate", @@ -77,7 +90,7 @@ describe("Adding a user to a team", () => { await supertest(app) .put("/team/123/add-member") - .set(authHeader()) + .set(auth) .send({ userId: 123, role: "teamEditor", @@ -95,7 +108,7 @@ describe("Adding a user to a team", () => { await supertest(app) .put("/team/123/add-member") - .set(authHeader()) + .set(auth) .send({ userId: 123, role: "teamEditor", @@ -122,7 +135,7 @@ describe("Removing a user from a team", () => { it("validates that userId is required", async () => { await supertest(app) .delete("/team/123/remove-member") - .set(authHeader()) + .set(auth) .send({}) .expect(400) .then((res) => { @@ -136,7 +149,7 @@ describe("Removing a user from a team", () => { await supertest(app) .delete("/team/123/remove-member") - .set(authHeader()) + .set(auth) .send({ userId: 123, }) @@ -153,7 +166,7 @@ describe("Removing a user from a team", () => { await supertest(app) .delete("/team/123/remove-member") - .set(authHeader()) + .set(auth) .send({ userId: 123, role: "teamEditor", @@ -181,7 +194,7 @@ describe("Changing a user's role", () => { it("validates that userId is required", async () => { await supertest(app) .patch("/team/123/change-member-role") - .set(authHeader()) + .set(auth) .send({ role: "teamEditor", }) @@ -195,7 +208,7 @@ describe("Changing a user's role", () => { it("validates that role is required", async () => { await supertest(app) .patch("/team/123/change-member-role") - .set(authHeader()) + .set(auth) .send({ userId: 123, }) @@ -209,7 +222,7 @@ describe("Changing a user's role", () => { it("validates that role is an accepted value", async () => { await supertest(app) .patch("/team/123/change-member-role") - .set(authHeader()) + .set(auth) .send({ userId: 123, role: "professor", @@ -226,7 +239,7 @@ describe("Changing a user's role", () => { await supertest(app) .patch("/team/123/change-member-role") - .set(authHeader()) + .set(auth) .send({ userId: 123, role: "teamEditor", @@ -244,7 +257,7 @@ describe("Changing a user's role", () => { await supertest(app) .patch("/team/123/change-member-role") - .set(authHeader()) + .set(auth) .send({ userId: 123, role: "teamEditor", diff --git a/api.planx.uk/modules/team/routes.ts b/api.planx.uk/modules/team/routes.ts index 3d428d6d91..8d2c54c872 100644 --- a/api.planx.uk/modules/team/routes.ts +++ b/api.planx.uk/modules/team/routes.ts @@ -5,7 +5,7 @@ import { validate } from "../../shared/middleware/validate"; const router = Router(); -router.use(AuthMiddleware.useJWT); +router.use(AuthMiddleware.usePlatformAdminAuth); router.put( "/:teamId/add-member", validate(Controller.upsertMemberSchema), diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index ea468ecfa9..8b7b574e72 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -35,7 +35,8 @@ import { useFilePermission, useHasuraAuth, useSendEmailAuth, - useJWT, + usePlatformAdminAuth, + useTeamEditorAuth, } from "./modules/auth/middleware"; import airbrake from "./airbrake"; @@ -249,11 +250,7 @@ app.get("/hasura", async function (_req, res, next) { * type: string * example: 2023-08-11T11:28:38.237493+00:00 */ -app.get("/me", useJWT, async function (req, res, next) { - // useJWT will return 401 if the JWT is missing or malformed - if (!req.user?.sub) - next({ status: 401, message: "User ID missing from JWT" }); - +app.get("/me", usePlatformAdminAuth, async function (req, res, next) { try { const user = await adminClient.request( gql` @@ -280,7 +277,7 @@ app.get("/me", useJWT, async function (req, res, next) { } }); -app.get("/gis", (_req, res, next) => { +app.get("/gis", (_req, _res, next) => { next({ status: 400, message: "Please specify a local authority", @@ -316,7 +313,7 @@ app.get("/", (_req, res) => { res.json({ hello: "world" }); }); -app.use("/admin", useJWT); +app.use("/admin", usePlatformAdminAuth); app.get("/admin/feedback", downloadFeedbackCSV); app.get("/admin/session/:sessionId/xml", getOneAppXML); app.get("/admin/session/:sessionId/bops", getBOPSPayload); @@ -333,13 +330,13 @@ app.get("/throw-error", () => { throw new Error("custom error"); }); -app.post("/flows/:flowId/copy", useJWT, copyFlow); +app.post("/flows/:flowId/copy", useTeamEditorAuth, copyFlow); -app.post("/flows/:flowId/diff", useJWT, validateAndDiffFlow); +app.post("/flows/:flowId/diff", useTeamEditorAuth, validateAndDiffFlow); -app.post("/flows/:flowId/move/:teamSlug", useJWT, moveFlow); +app.post("/flows/:flowId/move/:teamSlug", useTeamEditorAuth, moveFlow); -app.post("/flows/:flowId/publish", useJWT, publishFlow); +app.post("/flows/:flowId/publish", useTeamEditorAuth, publishFlow); /** * @swagger @@ -390,9 +387,13 @@ app.post("/flows/:flowId/publish", useJWT, publishFlow); * items: * type: string */ -app.post("/flows/:flowId/search", useJWT, findAndReplaceInFlow); +app.post("/flows/:flowId/search", usePlatformAdminAuth, findAndReplaceInFlow); -app.get("/flows/:flowId/copy-portal/:portalNodeId", useJWT, copyPortalAsFlow); +app.get( + "/flows/:flowId/copy-portal/:portalNodeId", + usePlatformAdminAuth, + copyPortalAsFlow, +); // unauthenticated because accessing flow schema only, no user data app.get("/flows/:flowId/download-schema", async (req, res, next) => { @@ -615,7 +616,7 @@ declare global { interface User { jwt: string; sub?: string; - "https://hasura.io/jwt/claims": { + "https://hasura.io/jwt/claims"?: { "x-hasura-allowed-roles": Role[]; }; } diff --git a/api.planx.uk/tests/mockJWT.js b/api.planx.uk/tests/mockJWT.js deleted file mode 100644 index 30fa25c1c9..0000000000 --- a/api.planx.uk/tests/mockJWT.js +++ /dev/null @@ -1,20 +0,0 @@ -import { sign } from "jsonwebtoken"; - -function getJWT(userId) { - const data = { - sub: String(userId), - "https://hasura.io/jwt/claims": { - "x-hasura-allowed-roles": ["admin"], - "x-hasura-default-role": "admin", - "x-hasura-user-id": String(userId), - }, - }; - - return sign(data, process.env.JWT_SECRET); -} - -function authHeader(userId) { - return { Authorization: `Bearer ${getJWT(userId || 0)}` }; -} - -export { authHeader, getJWT }; diff --git a/api.planx.uk/tests/mockJWT.ts b/api.planx.uk/tests/mockJWT.ts new file mode 100644 index 0000000000..706c834b12 --- /dev/null +++ b/api.planx.uk/tests/mockJWT.ts @@ -0,0 +1,21 @@ +import { Role } from "@opensystemslab/planx-core/types"; +import { sign } from "jsonwebtoken"; + +function getJWT({ role }: { role: Role }) { + const data = { + sub: "123", + "https://hasura.io/jwt/claims": { + "x-hasura-allowed-roles": [role], + "x-hasura-default-role": role, + "x-hasura-user-id": "123", + }, + }; + + return sign(data, process.env.JWT_SECRET!); +} + +function authHeader({ role }: { role: Role }) { + return { Authorization: `Bearer ${getJWT({ role })}` }; +} + +export { authHeader, getJWT };