Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add an audit table for applications submitted via S3 and Power Automate #3397

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api.planx.uk/modules/send/s3/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
72 changes: 56 additions & 16 deletions api.planx.uk/modules/send/s3/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -73,20 +75,56 @@ 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<CreateS3Application>(
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(
`Failed to send submission notification to ${localAuthority}'s Power Automate Webhook (${sessionId}): ${error}`,
);
});

// 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",
Expand All @@ -100,4 +138,6 @@ export async function sendToS3(
}`,
});
}
}
};

export { sendToS3 };
40 changes: 40 additions & 0 deletions hasura.planx.uk/metadata/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE "public.s3_applications" CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE "public"."s3_applications" (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that these applications will be automatically picked up by the submission_services_log view as that's reading Hasura metadata tables directly right?

However, I think that the submission_services_summary view will require an update to also read from this new table.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question - you're right:

  • submission_services_log is automatically picking up these records because it reads from Hasura events
  • submission_services_summary will need to be manually updated (then Metabase too?) to join to this new audit table so analytics can be correctly tracked

I'd prefer to pick this up in a follow-up PR !

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, makes total sense! ✅

"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';
79 changes: 79 additions & 0 deletions hasura.planx.uk/tests/s3_applications.test.js
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
Loading