From be4bb9d2292b8f8e2d3118148ec6c816aca3561b Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Wed, 10 Jul 2024 13:37:09 +0200 Subject: [PATCH] feat: add an audit table for applications submitted via S3 and Power Automate (#3397) --- api.planx.uk/modules/send/s3/index.test.ts | 8 ++ api.planx.uk/modules/send/s3/index.ts | 72 +++++++++++++---- hasura.planx.uk/metadata/tables.yaml | 40 ++++++++++ .../down.sql | 1 + .../up.sql | 11 +++ hasura.planx.uk/tests/s3_applications.test.js | 79 +++++++++++++++++++ 6 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/down.sql create mode 100644 hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/up.sql create mode 100644 hasura.planx.uk/tests/s3_applications.test.js diff --git a/api.planx.uk/modules/send/s3/index.test.ts b/api.planx.uk/modules/send/s3/index.test.ts index 9db576188e..352c8c07ef 100644 --- a/api.planx.uk/modules/send/s3/index.test.ts +++ b/api.planx.uk/modules/send/s3/index.test.ts @@ -53,6 +53,14 @@ describe(`uploading an application to S3`, () => { slug: "unsupported-team", }, }); + + queryMock.mockQuery({ + name: "CreateS3Application", + matchOnVariables: false, + data: { + insertS3Application: { id: 1 }, + }, + }); }); it("requires auth", async () => { diff --git a/api.planx.uk/modules/send/s3/index.ts b/api.planx.uk/modules/send/s3/index.ts index f301729334..39aed61780 100644 --- a/api.planx.uk/modules/send/s3/index.ts +++ b/api.planx.uk/modules/send/s3/index.ts @@ -1,16 +1,19 @@ +import axios from "axios"; import type { NextFunction, Request, Response } from "express"; +import { gql } from "graphql-request"; import { $api } from "../../../client"; +import { Passport } from "../../../types"; import { uploadPrivateFile } from "../../file/service/uploadFile"; import { markSessionAsSubmitted } from "../../saveAndReturn/service/utils"; -import axios from "axios"; import { isApplicationTypeSupported } from "../utils/helpers"; -import { Passport } from "../../../types"; -export async function sendToS3( - req: Request, - res: Response, - next: NextFunction, -) { +interface CreateS3Application { + insertS3Application: { + id: string; + }; +} + +const sendToS3 = async (req: Request, res: Response, next: NextFunction) => { // `/upload-submission/:localAuthority` is only called via Hasura's scheduled event webhook, so body is wrapped in a "payload" key const { payload } = req.body; const localAuthority = req.params.localAuthority; @@ -58,8 +61,7 @@ export async function sendToS3( ); // Send a notification with the file URL to the Power Automate webook - let webhookResponseStatus: number | undefined; - await axios({ + const webhookRequest = { method: "POST", url: powerAutomateWebhookURL, adapter: "http", @@ -73,10 +75,49 @@ export async function sendToS3( file: fileUrl, payload: doValidation ? "Validated ODP Schema" : "Discretionary", }, - }) - .then((res) => { - // TODO Create & update audit table entry here + }; + let webhookResponseStatus: number | undefined; + await axios(webhookRequest) + .then(async (res) => { webhookResponseStatus = res.status; + + // Mark session as submitted so that reminder and expiry emails are not triggered + markSessionAsSubmitted(sessionId); + + // Create an audit entry + const applicationId = await $api.client.request( + gql` + mutation CreateS3Application( + $session_id: String! + $team_slug: String! + $webhook_request: jsonb! + $webhook_response: jsonb = {} + ) { + insertS3Application: insert_s3_applications_one( + object: { + session_id: $session_id + team_slug: $team_slug + webhook_request: $webhook_request + webhook_response: $webhook_response + } + ) { + id + } + } + `, + { + session_id: sessionId, + team_slug: localAuthority, + webhook_request: webhookRequest, + webhook_response: res, + }, + ); + + return { + application: { + ...applicationId.insertS3Application, + }, + }; }) .catch((error) => { throw new Error( @@ -84,9 +125,6 @@ export async function sendToS3( ); }); - // Mark session as submitted so that reminder and expiry emails are not triggered - markSessionAsSubmitted(sessionId); - return res.status(200).send({ message: `Successfully uploaded submission to S3: ${fileUrl}`, payload: doValidation ? "Validated ODP Schema" : "Discretionary", @@ -100,4 +138,6 @@ export async function sendToS3( }`, }); } -} +}; + +export { sendToS3 }; diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index 49a7278c64..a20b9f2d82 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -1414,6 +1414,46 @@ - role: api permission: filter: {} +- table: + name: s3_applications + schema: public + insert_permissions: + - role: api + permission: + check: {} + columns: + - id + - webhook_request + - webhook_response + - session_id + - team_slug + - created_at + comment: "" + select_permissions: + - role: api + permission: + columns: + - id + - webhook_request + - webhook_response + - session_id + - team_slug + - created_at + filter: {} + comment: "" + update_permissions: + - role: api + permission: + columns: + - id + - webhook_request + - webhook_response + - session_id + - team_slug + - created_at + filter: {} + check: null + comment: "" - table: name: sessions schema: public diff --git a/hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/down.sql b/hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/down.sql new file mode 100644 index 0000000000..d2db0a6dcf --- /dev/null +++ b/hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/down.sql @@ -0,0 +1 @@ +DROP TABLE "public.s3_applications" CASCADE; diff --git a/hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/up.sql b/hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/up.sql new file mode 100644 index 0000000000..b5995aeb1c --- /dev/null +++ b/hasura.planx.uk/migrations/1720597665798_create_table_s3_applications/up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "public"."s3_applications" ( + "id" serial NOT NULL, + "session_id" text NOT NULL, + "team_slug" text NOT NULL, + "webhook_request" JSONB NOT NULL, + "webhook_response" JSONB NOT NULL, + "created_at" timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY ("id") +); + +COMMENT ON TABLE "public"."s3_applications" IS 'Stores a receipt of applications submitted using the Upload to AWS S3 method with notifications via Power Automate webhook'; diff --git a/hasura.planx.uk/tests/s3_applications.test.js b/hasura.planx.uk/tests/s3_applications.test.js new file mode 100644 index 0000000000..913e95cb8c --- /dev/null +++ b/hasura.planx.uk/tests/s3_applications.test.js @@ -0,0 +1,79 @@ +const { introspectAs } = require("./utils"); + +describe("s3_applications", () => { + describe("public", () => { + let i; + beforeAll(async () => { + i = await introspectAs("public"); + }); + + test("cannot query s3 applications", () => { + expect(i.queries).not.toContain("s3_applications"); + }); + + test("cannot create, update, or delete s3 applications", () => { + expect(i).toHaveNoMutationsFor("s3_applications"); + }); + }); + + describe("admin", () => { + let i; + beforeAll(async () => { + i = await introspectAs("admin"); + }); + + test("has full access to query and mutate s3 applications", () => { + expect(i.queries).toContain("s3_applications"); + expect(i.mutations).toContain("insert_s3_applications"); + expect(i.mutations).toContain("update_s3_applications_by_pk"); + expect(i.mutations).toContain("delete_s3_applications"); + }); + }); + + describe("platformAdmin", () => { + let i; + beforeAll(async () => { + i = await introspectAs("platformAdmin"); + }); + + test("cannot query s3_applications", () => { + expect(i.queries).not.toContain("s3_applications"); + }); + + test("cannot create, update, or delete s3_applications", () => { + expect(i).toHaveNoMutationsFor("s3_applications"); + }); + }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query s3_applications", () => { + expect(i.queries).not.toContain("s3_applications"); + }); + + test("cannot create, update, or delete s3_applications", () => { + expect(i).toHaveNoMutationsFor("s3_applications"); + }); + }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("can query and mutate s3 applications", () => { + expect(i.queries).toContain("s3_applications"); + expect(i.mutations).toContain("insert_s3_applications"); + expect(i.mutations).toContain("update_s3_applications_by_pk"); + }); + + test("cannot delete s3 applications", () => { + expect(i.mutations).not.toContain("delete_s3_applications"); + }); + }); +});