diff --git a/api.planx.uk/modules/auth/service.ts b/api.planx.uk/modules/auth/service.ts index 1ee77b0afa..e6201b1170 100644 --- a/api.planx.uk/modules/auth/service.ts +++ b/api.planx.uk/modules/auth/service.ts @@ -1,34 +1,49 @@ import { sign } from "jsonwebtoken"; -import { adminGraphQLClient as adminClient } from "../../hasura"; -import { gql } from "graphql-request"; +import { $admin } from "../../client"; +import { User, Role } from "@opensystemslab/planx-core/types"; -export const buildJWT = async (email: string | undefined) => { - const { users } = await adminClient.request( - gql` - query ($email: String!) { - users(where: { email: { _eq: $email } }, limit: 1) { - id - } - } - `, - { email }, - ); - - if (!users.length) return; - - const { id } = users[0]; - - const hasura = { - "x-hasura-allowed-roles": ["platformAdmin", "public"], - "x-hasura-default-role": "platformAdmin", - "x-hasura-user-id": id.toString(), - }; +export const buildJWT = async (email: string): Promise => { + const user = await $admin.user.getByEmail(email); + if (!user) return; const data = { - sub: id.toString(), - "https://hasura.io/jwt/claims": hasura, + sub: user.id.toString(), + "https://hasura.io/jwt/claims": generateHasuraClaimsForUser(user), }; const jwt = sign(data, process.env.JWT_SECRET!); return jwt; }; + +const generateHasuraClaimsForUser = (user: User) => ({ + "x-hasura-allowed-roles": getAllowedRolesForUser(user), + "x-hasura-default-role": getDefaultRoleForUser(user), + "x-hasura-user-id": user.id.toString(), +}); + +/** + * Get all possible roles for this user + * Requests made outside this scope will not be authorised by Hasura + */ +const getAllowedRolesForUser = (user: User): Role[] => { + const teamRoles = user.teams.map((teamRole) => teamRole.role); + const allowedRoles: Role[] = [ + "public", // Allow public access + "teamEditor", // Least privileged role for authenticated users - required for Editor access + ...teamRoles, // User specific roles + ]; + if (user.isPlatformAdmin) allowedRoles.push("platformAdmin"); + + return [...new Set(allowedRoles)]; +}; + +/** + * The default role is used for all requests + * Can be overwritten on a per-request basis in the client using the x-hasura-role header + * set to a role in the x-hasura-allowed-roles list + * + * This is the role of least privilege for the user + */ +const getDefaultRoleForUser = (user: User): Role => { + return user.isPlatformAdmin ? "platformAdmin" : "teamEditor"; +}; diff --git a/api.planx.uk/modules/auth/strategy/google.ts b/api.planx.uk/modules/auth/strategy/google.ts index 10212ba2d5..f9c6632afd 100644 --- a/api.planx.uk/modules/auth/strategy/google.ts +++ b/api.planx.uk/modules/auth/strategy/google.ts @@ -9,6 +9,8 @@ export const googleStrategy = new GoogleStrategy( }, async function (_accessToken, _refreshToken, profile, done) { const { email } = profile._json; + if (!email) throw Error("Unable to authenticate without email"); + const jwt = await buildJWT(email); if (!jwt) { diff --git a/api.planx.uk/modules/team/controller.ts b/api.planx.uk/modules/team/controller.ts index 6f1b3a93fe..e6eabd9e8c 100644 --- a/api.planx.uk/modules/team/controller.ts +++ b/api.planx.uk/modules/team/controller.ts @@ -12,7 +12,7 @@ export const upsertMemberSchema = z.object({ }), body: z.object({ userId: z.number(), - role: z.enum(["teamAdmin", "teamViewer"]), + role: z.enum(["teamEditor", "teamViewer"]), }), }); diff --git a/api.planx.uk/modules/team/docs.yaml b/api.planx.uk/modules/team/docs.yaml index e4d6f2740d..06cff793e5 100644 --- a/api.planx.uk/modules/team/docs.yaml +++ b/api.planx.uk/modules/team/docs.yaml @@ -21,7 +21,7 @@ components: example: 123 role: type: string - enum: ["teamViewer", "teamAdmin"] + enum: ["teamViewer", "teamEditor"] paths: /team/{teamId}/add-member: put: diff --git a/api.planx.uk/modules/team/index.test.ts b/api.planx.uk/modules/team/index.test.ts index 8c0365838e..03dc9e110f 100644 --- a/api.planx.uk/modules/team/index.test.ts +++ b/api.planx.uk/modules/team/index.test.ts @@ -57,7 +57,7 @@ describe("Adding a user to a team", () => { }); }); - it("validates that role must one an accepted value", async () => { + it("validates that role must be an accepted value", async () => { await supertest(app) .put("/team/123/add-member") .set(authHeader()) @@ -80,7 +80,7 @@ describe("Adding a user to a team", () => { .set(authHeader()) .send({ userId: 123, - role: "teamAdmin", + role: "teamEditor", }) .expect(500) .then((res) => { @@ -98,7 +98,7 @@ describe("Adding a user to a team", () => { .set(authHeader()) .send({ userId: 123, - role: "teamAdmin", + role: "teamEditor", }) .expect(200) .then((res) => { @@ -156,7 +156,7 @@ describe("Removing a user from a team", () => { .set(authHeader()) .send({ userId: 123, - role: "teamAdmin", + role: "teamEditor", }) .expect(200) .then((res) => { @@ -173,7 +173,7 @@ describe("Changing a user's role", () => { .patch("/team/123/change-member-role") .send({ userId: 123, - role: "teamAdmin", + role: "teamEditor", }) .expect(401); }); @@ -183,7 +183,7 @@ describe("Changing a user's role", () => { .patch("/team/123/change-member-role") .set(authHeader()) .send({ - role: "teamAdmin", + role: "teamEditor", }) .expect(400) .then((res) => { @@ -229,7 +229,7 @@ describe("Changing a user's role", () => { .set(authHeader()) .send({ userId: 123, - role: "teamAdmin", + role: "teamEditor", }) .expect(500) .then((res) => { @@ -247,7 +247,7 @@ describe("Changing a user's role", () => { .set(authHeader()) .send({ userId: 123, - role: "teamAdmin", + role: "teamEditor", }) .expect(200) .then((res) => { diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index f771661596..66a7c8207b 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@airbrake/node": "^2.1.8", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#f6fcac9", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4e3d09f", "@types/isomorphic-fetch": "^0.0.36", "adm-zip": "^0.5.10", "aws-sdk": "^2.1441.0", diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index f76e5c762e..753ac87739 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^2.1.8 version: 2.1.8 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#f6fcac9 - version: github.com/theopensystemslab/planx-core/dfaf533 + specifier: git+https://github.com/theopensystemslab/planx-core#4e3d09f + version: github.com/theopensystemslab/planx-core/4e3d09f '@types/isomorphic-fetch': specifier: ^0.0.36 version: 0.0.36 @@ -8050,8 +8050,8 @@ packages: resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} dev: false - github.com/theopensystemslab/planx-core/dfaf533: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/dfaf533} + github.com/theopensystemslab/planx-core/4e3d09f: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4e3d09f} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/api.planx.uk/tests/mockJWT.js b/api.planx.uk/tests/mockJWT.js index 30fa25c1c9..89b494a16f 100644 --- a/api.planx.uk/tests/mockJWT.js +++ b/api.planx.uk/tests/mockJWT.js @@ -4,8 +4,8 @@ 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-allowed-roles": ["platformAdmin", "public"], + "x-hasura-default-role": "platformAdmin", "x-hasura-user-id": String(userId), }, }; diff --git a/e2e/tests/api-driven/package.json b/e2e/tests/api-driven/package.json index 165ae7dbb3..de91478a3e 100644 --- a/e2e/tests/api-driven/package.json +++ b/e2e/tests/api-driven/package.json @@ -6,7 +6,7 @@ }, "dependencies": { "@cucumber/cucumber": "^9.3.0", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#f6fcac9", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4e3d09f", "axios": "^1.4.0", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", diff --git a/e2e/tests/api-driven/pnpm-lock.yaml b/e2e/tests/api-driven/pnpm-lock.yaml index 09336e52e4..199b1e8691 100644 --- a/e2e/tests/api-driven/pnpm-lock.yaml +++ b/e2e/tests/api-driven/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#f6fcac9 - version: github.com/theopensystemslab/planx-core/dfaf533 + specifier: git+https://github.com/theopensystemslab/planx-core#4e3d09f + version: github.com/theopensystemslab/planx-core/4e3d09f axios: specifier: ^1.4.0 version: 1.4.0 @@ -2498,8 +2498,8 @@ packages: resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} dev: false - github.com/theopensystemslab/planx-core/dfaf533: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/dfaf533} + github.com/theopensystemslab/planx-core/4e3d09f: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4e3d09f} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/e2e/tests/ui-driven/package.json b/e2e/tests/ui-driven/package.json index a20e66118e..eead6768e2 100644 --- a/e2e/tests/ui-driven/package.json +++ b/e2e/tests/ui-driven/package.json @@ -8,7 +8,7 @@ "postinstall": "./install-dependencies.sh" }, "dependencies": { - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#f6fcac9", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4e3d09f", "axios": "^1.4.0", "dotenv": "^16.3.1", "eslint": "^8.44.0", diff --git a/e2e/tests/ui-driven/pnpm-lock.yaml b/e2e/tests/ui-driven/pnpm-lock.yaml index 2c274a0f7c..7a9edbc70f 100644 --- a/e2e/tests/ui-driven/pnpm-lock.yaml +++ b/e2e/tests/ui-driven/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#f6fcac9 - version: github.com/theopensystemslab/planx-core/dfaf533 + specifier: git+https://github.com/theopensystemslab/planx-core#4e3d09f + version: github.com/theopensystemslab/planx-core/4e3d09f axios: specifier: ^1.4.0 version: 1.4.0 @@ -2367,8 +2367,8 @@ packages: resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} dev: false - github.com/theopensystemslab/planx-core/dfaf533: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/dfaf533} + github.com/theopensystemslab/planx-core/4e3d09f: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4e3d09f} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/e2e/tests/ui-driven/src/context.ts b/e2e/tests/ui-driven/src/context.ts index 10e6d03e49..3e547e86d0 100644 --- a/e2e/tests/ui-driven/src/context.ts +++ b/e2e/tests/ui-driven/src/context.ts @@ -108,8 +108,8 @@ export function generateAuthenticationToken(userId) { { sub: `${userId}`, "https://hasura.io/jwt/claims": { - "x-hasura-allowed-roles": ["admin"], - "x-hasura-default-role": "admin", + "x-hasura-allowed-roles": ["platformAdmin", "public"], + "x-hasura-default-role": "platformAdmin", "x-hasura-user-id": `${userId}`, }, }, diff --git a/e2e/tests/ui-driven/src/utils.ts b/e2e/tests/ui-driven/src/utils.ts index 583f929095..6e6036fc51 100644 --- a/e2e/tests/ui-driven/src/utils.ts +++ b/e2e/tests/ui-driven/src/utils.ts @@ -29,8 +29,8 @@ export const getJWT = (userId) => { const data = { sub: String(userId), "https://hasura.io/jwt/claims": { - "x-hasura-allowed-roles": ["admin"], - "x-hasura-default-role": "admin", + "x-hasura-allowed-roles": ["platformAdmin", "public"], + "x-hasura-default-role": "platformAdmin", "x-hasura-user-id": String(userId), }, }; diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index 6244eb504b..ffec422e0c 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -14,7 +14,7 @@ "@mui/styles": "^5.14.5", "@mui/utils": "^5.14.5", "@opensystemslab/map": "^0.7.5", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#f6fcac9", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4e3d09f", "@tiptap/core": "^2.0.3", "@tiptap/extension-bold": "^2.0.3", "@tiptap/extension-bubble-menu": "^2.1.6", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index 443b048ad7..4625024832 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -46,8 +46,8 @@ dependencies: specifier: ^0.7.5 version: 0.7.5 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#f6fcac9 - version: git/github.com+theopensystemslab/planx-core/f6fcac9(@types/react@18.2.20) + specifier: git+https://github.com/theopensystemslab/planx-core#4e3d09f + version: github.com/theopensystemslab/planx-core/4e3d09f(@types/react@18.2.20) '@tiptap/core': specifier: ^2.0.3 version: 2.0.3(@tiptap/pm@2.0.3) @@ -20690,9 +20690,9 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - git/github.com+theopensystemslab/planx-core/f6fcac9(@types/react@18.2.20): - resolution: {commit: f6fcac9, repo: git@github.com:theopensystemslab/planx-core.git, type: git} - id: git@github.com+theopensystemslab/planx-core/f6fcac9 + github.com/theopensystemslab/planx-core/4e3d09f(@types/react@18.2.20): + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4e3d09f} + id: github.com/theopensystemslab/planx-core/4e3d09f name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/editor.planx.uk/src/lib/graphql.ts b/editor.planx.uk/src/lib/graphql.ts index c2e88d6086..0a20c4d490 100644 --- a/editor.planx.uk/src/lib/graphql.ts +++ b/editor.planx.uk/src/lib/graphql.ts @@ -1,6 +1,7 @@ import { ApolloClient, createHttpLink, + DefaultContext, from, InMemoryCache, } from "@apollo/client"; @@ -88,3 +89,13 @@ export const publicClient = new ApolloClient({ link: from([retryLink, errorLink, publicHttpLink]), cache: new InMemoryCache(), }); + +/** + * Explicitly connect to Hasura using the "public" role + * Allows authenticated users with a different x-hasura-default-role (e.g. teamEditor, platformAdmin) to access public resources + */ +export const publicContext: DefaultContext = { + headers: { + "x-hasura-role": "public" + } +} diff --git a/editor.planx.uk/src/lib/lowcalStorage.ts b/editor.planx.uk/src/lib/lowcalStorage.ts index f2619b4e01..56d14bad20 100644 --- a/editor.planx.uk/src/lib/lowcalStorage.ts +++ b/editor.planx.uk/src/lib/lowcalStorage.ts @@ -1,4 +1,4 @@ -import { gql } from "@apollo/client"; +import { DefaultContext, gql } from "@apollo/client"; import { useStore } from "pages/FlowEditor/lib/store"; import { Session } from "types"; @@ -20,7 +20,7 @@ class LowcalStorage { } `, variables: { id }, - ...getPublicContext(id), + context: getSessionContext(id), }); try { @@ -49,7 +49,7 @@ class LowcalStorage { } `, variables: { id }, - ...getPublicContext(id), + context: getSessionContext(id), }); }); @@ -90,7 +90,7 @@ class LowcalStorage { email: useStore.getState().saveToEmail || "", flowId: useStore.getState().id, }, - ...getPublicContext(id), + context: getSessionContext(id), }); }); } @@ -130,15 +130,13 @@ export const stringifyWithRootKeysSortedAlphabetically = ( * Generate context for GraphQL client Save & Return requests * Hasura "Public" role users need the sessionId and email for lowcal_sessions access */ -const getPublicContext = (sessionId: string) => ({ - context: { - headers: { - "x-hasura-lowcal-session-id": sessionId, - "x-hasura-lowcal-email": - // email may be absent for non save and return journeys - useStore.getState().saveToEmail?.toLowerCase() || "", - }, - }, +const getSessionContext = (sessionId: string): DefaultContext => ({ + headers: { + "x-hasura-lowcal-session-id": sessionId, + "x-hasura-lowcal-email": + // email may be absent for non save and return journeys + useStore.getState().saveToEmail?.toLowerCase() || "", + } }); /** diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index a60d88180b..73706e7cbe 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -176,6 +176,42 @@ schema: public name: compile_flow_portals comment: Flow data with portals merged in + insert_permissions: + - role: platformAdmin + permission: + check: {} + columns: + - creator_id + - team_id + - settings + - slug + - created_at + - updated_at + - copied_from + - id + - version + - data + - role: teamEditor + permission: + check: + team: + members: + _and: + - user_id: + _eq: x-hasura-user-id + - role: + _eq: teamEditor + columns: + - creator_id + - team_id + - settings + - slug + - created_at + - updated_at + - copied_from + - id + - version + - data select_permissions: - role: platformAdmin permission: @@ -207,6 +243,21 @@ computed_fields: - data_merged filter: {} + - role: teamEditor + permission: + columns: + - created_at + - creator_id + - data + - id + - settings + - slug + - team_id + - updated_at + - version + computed_fields: + - data_merged + filter: {} update_permissions: - role: platformAdmin permission: @@ -216,6 +267,33 @@ - slug filter: {} check: null + - role: teamEditor + permission: + columns: + - data + - settings + - slug + filter: + team: + members: + _and: + - user_id: + _eq: x-hasura-user-id + - role: + _eq: teamEditor + check: null + delete_permissions: + - role: teamEditor + permission: + backend_only: false + filter: + team: + members: + _and: + - user_id: + _eq: x-hasura-user-id + - role: + _eq: teamEditor - table: schema: public name: global_settings @@ -238,6 +316,12 @@ columns: - footer_content filter: {} + - role: teamEditor + permission: + columns: + - footer_content + - id + filter: {} update_permissions: - role: platformAdmin permission: @@ -440,6 +524,17 @@ - created_at - updated_at - flow_id + - role: teamEditor + permission: + check: {} + columns: + - id + - actor_id + - version + - data + - created_at + - updated_at + - flow_id select_permissions: - role: platformAdmin permission: @@ -452,12 +547,28 @@ - created_at - updated_at filter: {} + - role: teamEditor + permission: + columns: + - id + - flow_id + - version + - actor_id + - data + - created_at + - updated_at + filter: {} update_permissions: - role: platformAdmin permission: columns: [] filter: {} check: null + - role: teamEditor + permission: + columns: [] + filter: {} + check: null - table: schema: public name: payment_requests @@ -644,6 +755,24 @@ - created_at - flow_id - data + - role: teamEditor + permission: + check: + flow: + team: + members: + _and: + - user_id: + _eq: x-hasura-user-id + - role: + _eq: teamEditor + columns: + - id + - publisher_id + - summary + - created_at + - flow_id + - data select_permissions: - role: platformAdmin permission: @@ -665,6 +794,16 @@ - publisher_id - summary filter: {} + - role: teamEditor + permission: + columns: + - created_at + - data + - flow_id + - id + - publisher_id + - summary + filter: {} - table: schema: public name: reconciliation_requests @@ -771,16 +910,17 @@ permission: check: {} columns: + - boundary + - created_at + - domain - id + - name - notify_personalisation - settings - - theme - - domain - - name - slug - - created_at - - updated_at - submission_email + - theme + - updated_at select_permissions: - role: platformAdmin permission: @@ -811,6 +951,19 @@ computed_fields: - boundary_bbox filter: {} + - role: teamEditor + permission: + columns: + - created_at + - domain + - id + - name + - notify_personalisation + - settings + - slug + - theme + - updated_at + filter: {} update_permissions: - role: platformAdmin permission: @@ -889,3 +1042,9 @@ - updated_at - is_platform_admin filter: {} + - role: teamEditor + permission: + columns: + - first_name + - last_name + filter: {} diff --git a/hasura.planx.uk/seeds/1595528762802_teams_and_users.sql b/hasura.planx.uk/seeds/1595528762802_teams_and_users.sql index a9f232dbab..1728178b6d 100644 --- a/hasura.planx.uk/seeds/1595528762802_teams_and_users.sql +++ b/hasura.planx.uk/seeds/1595528762802_teams_and_users.sql @@ -1,6 +1,6 @@ -INSERT INTO public.users (id, first_name, last_name, email) VALUES (2, 'Alastair', 'Parvin', 'alastair@opensystemslab.io') ON CONFLICT (id) DO NOTHING; -INSERT INTO public.users (id, first_name, last_name, email) VALUES (20, 'Jessica', 'McInchak', 'jessica@opensystemslab.io') ON CONFLICT (id) DO NOTHING; -INSERT INTO public.users (id, first_name, last_name, email) VALUES (33, 'Dafydd', 'Pearson', 'dafydd@opensystemslab.io') ON CONFLICT (id) DO NOTHING; -INSERT INTO public.users (id, first_name, last_name, email) VALUES (65, 'Ian', 'Jones', 'ian@opensystemslab.io') ON CONFLICT (id) DO NOTHING; +INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (2, 'Alastair', 'Parvin', 'alastair@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; +INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (20, 'Jessica', 'McInchak', 'jessica@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; +INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (33, 'Dafydd', 'Pearson', 'dafydd@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; +INSERT INTO public.users (id, first_name, last_name, email, is_platform_admin) VALUES (65, 'Ian', 'Jones', 'ian@opensystemslab.io', true) ON CONFLICT (id) DO NOTHING; SELECT setval('users_id_seq', max(id)) FROM users; SELECT setval('teams_id_seq', max(id)) FROM teams; diff --git a/hasura.planx.uk/tests/analytics.test.js b/hasura.planx.uk/tests/analytics.test.js index a23fc8d9f6..53bfb9c10b 100644 --- a/hasura.planx.uk/tests/analytics.test.js +++ b/hasura.planx.uk/tests/analytics.test.js @@ -54,4 +54,19 @@ describe("analytics and analytics_logs", () => { expect(i).toHaveNoMutationsFor("analytics_logs"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query analytics_logs", () => { + expect(i.queries).not.toContain("analytics_logs"); + }); + + test("cannot create, update, or delete analytics_logs", () => { + expect(i).toHaveNoMutationsFor("analytics_logs"); + }); + }); }); diff --git a/hasura.planx.uk/tests/blpu_codes.test.js b/hasura.planx.uk/tests/blpu_codes.test.js index d2c74d83f6..ade04eb536 100644 --- a/hasura.planx.uk/tests/blpu_codes.test.js +++ b/hasura.planx.uk/tests/blpu_codes.test.js @@ -29,7 +29,6 @@ describe("blpu_codes", () => { expect(i.mutations).toContain("delete_blpu_codes"); }); }); - describe("platformAdmin", () => { let i; beforeAll(async () => { @@ -44,4 +43,19 @@ describe("blpu_codes", () => { expect(i).toHaveNoMutationsFor("blpu_codes"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("platformAdmin"); + }); + + test("cannot query blpu_codes", () => { + expect(i.queries).not.toContain("blpu_codes"); + }); + + test("cannot create, update, or delete blpu_codes", () => { + expect(i).toHaveNoMutationsFor("blpu_codes"); + }); + }); }); diff --git a/hasura.planx.uk/tests/bops_applications.test.js b/hasura.planx.uk/tests/bops_applications.test.js index 4489aacac2..dc6b2b909f 100644 --- a/hasura.planx.uk/tests/bops_applications.test.js +++ b/hasura.planx.uk/tests/bops_applications.test.js @@ -22,7 +22,7 @@ describe("bops_applications", () => { i = await introspectAs("admin"); }); - test("has full access to query and mutate bops appliations", () => { + test("has full access to query and mutate bops applications", () => { expect(i.queries).toContain("bops_applications"); expect(i.mutations).toContain("insert_bops_applications"); expect(i.mutations).toContain("update_bops_applications_by_pk"); @@ -36,12 +36,27 @@ describe("bops_applications", () => { i = await introspectAs("platformAdmin"); }); - test("cannot query bops_appliations", () => { - expect(i.queries).not.toContain("bops_appliations"); + test("cannot query bops_applications", () => { + expect(i.queries).not.toContain("bops_applications"); }); - test("cannot create, update, or delete bops_appliations", () => { - expect(i).toHaveNoMutationsFor("bops_appliations"); + test("cannot create, update, or delete bops_applications", () => { + expect(i).toHaveNoMutationsFor("bops_applications"); + }); + }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query bops_applications", () => { + expect(i.queries).not.toContain("bops_applications"); + }); + + test("cannot create, update, or delete bops_applications", () => { + expect(i).toHaveNoMutationsFor("bops_applications"); }); }); }); diff --git a/hasura.planx.uk/tests/email_applications.test.js b/hasura.planx.uk/tests/email_applications.test.js index 7bf7bbaead..b4a2cd711a 100644 --- a/hasura.planx.uk/tests/email_applications.test.js +++ b/hasura.planx.uk/tests/email_applications.test.js @@ -45,4 +45,19 @@ describe("email_applications", () => { expect(i).toHaveNoMutationsFor("email_applications"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query email_applications", () => { + expect(i.queries).not.toContain("email_applications"); + }); + + test("cannot create, update, or delete email_applications", () => { + expect(i).toHaveNoMutationsFor("email_applications"); + }); + }); }); diff --git a/hasura.planx.uk/tests/flows.test.js b/hasura.planx.uk/tests/flows.test.js index b74f2468c9..c7c0075612 100644 --- a/hasura.planx.uk/tests/flows.test.js +++ b/hasura.planx.uk/tests/flows.test.js @@ -37,7 +37,6 @@ describe("flows and operations", () => { expect(i.mutations).toContain("delete_operations"); }); }); - describe("platformAdmin", () => { let i; beforeAll(async () => { @@ -49,6 +48,59 @@ describe("flows and operations", () => { expect(i.queries).toContain("operations"); }); + test("can update flows", () => { + expect(i.mutations).toContain("update_flows_by_pk"); + expect(i.mutations).toContain("update_flows"); + }); + + test("can create flows", () => { + expect(i.mutations).toContain("insert_flows_one"); + expect(i.mutations).toContain("insert_flows"); + }); + + test("can query published flows", () => { + expect(i.queries).toContain("published_flows"); + }); + + test("can create published_flows", () => { + expect(i.mutations).toContain("insert_published_flows_one"); + expect(i.mutations).toContain("insert_published_flows"); + }); + + test("cannot update or delete published_flows", () => { + expect(i.mutations).not.toContain("delete_published_flows_by_pk"); + expect(i.mutations).not.toContain("delete_published_flows"); + expect(i.mutations).not.toContain("update_published_flows_by_pk"); + expect(i.mutations).not.toContain("update_published_flows"); + }); + }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("can query flows and their associated operations", () => { + expect(i.queries).toContain("flows"); + expect(i.queries).toContain("operations"); + }); + + test("can update flows", () => { + expect(i.mutations).toContain("update_flows_by_pk"); + expect(i.mutations).toContain("update_flows"); + }); + + test("can create flows", () => { + expect(i.mutations).toContain("insert_flows_one"); + expect(i.mutations).toContain("insert_flows"); + }); + + test("can delete flows", () => { + expect(i.mutations).toContain("delete_flows_by_pk"); + expect(i.mutations).toContain("delete_flows"); + }); + test("can query published flows", () => { expect(i.queries).toContain("published_flows"); }); diff --git a/hasura.planx.uk/tests/global_settings.test.js b/hasura.planx.uk/tests/global_settings.test.js index 324cdfc3c1..98d12e8bbc 100644 --- a/hasura.planx.uk/tests/global_settings.test.js +++ b/hasura.planx.uk/tests/global_settings.test.js @@ -48,4 +48,19 @@ describe("global_settings", () => { expect(i.mutations).not.toContain("delete_global_settings"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("can query global_settings view", () => { + expect(i.queries).toContain("global_settings"); + }); + + test("cannot create, update, or delete global_settings", () => { + expect(i).toHaveNoMutationsFor("global_settings"); + }); + }); }); diff --git a/hasura.planx.uk/tests/lowcal_sessions.test.js b/hasura.planx.uk/tests/lowcal_sessions.test.js index 6d7877e197..7de02f9e9d 100644 --- a/hasura.planx.uk/tests/lowcal_sessions.test.js +++ b/hasura.planx.uk/tests/lowcal_sessions.test.js @@ -443,4 +443,19 @@ describe("lowcal_sessions", () => { expect(i).toHaveNoMutationsFor("lowcal_sessions"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query lowcal_sessions", () => { + expect(i.queries).not.toContain("lowcal_sessions"); + }); + + test("cannot create, update, or delete lowcal_sessions", () => { + expect(i).toHaveNoMutationsFor("lowcal_sessions"); + }); + }); }); diff --git a/hasura.planx.uk/tests/package.json b/hasura.planx.uk/tests/package.json index b4b2360282..7aaf6663c3 100644 --- a/hasura.planx.uk/tests/package.json +++ b/hasura.planx.uk/tests/package.json @@ -1,7 +1,8 @@ { "scripts": { "test": "echo 'Running hasura tests…' && jest && echo 'Hasura tests passed.'", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "build-jwt": "DOTENV_CONFIG_PATH=../.env.test node -r dotenv/config scripts/buildJWT.js" }, "dependencies": { "dotenv": "^16.3.1", diff --git a/hasura.planx.uk/tests/payment_requests.test.js b/hasura.planx.uk/tests/payment_requests.test.js index 0328f6c545..45ecb2c4e5 100644 --- a/hasura.planx.uk/tests/payment_requests.test.js +++ b/hasura.planx.uk/tests/payment_requests.test.js @@ -112,6 +112,21 @@ describe("payment_requests", () => { expect(i).toHaveNoMutationsFor("payment_requests"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query payment_requests", () => { + expect(i.queries).not.toContain("payment_requests"); + }); + + test("cannot create, update, or delete payment_requests", () => { + expect(i).toHaveNoMutationsFor("payment_requests"); + }); + }); }); const insertSessions = async (sessionIds) => { diff --git a/hasura.planx.uk/tests/payment_status.test.js b/hasura.planx.uk/tests/payment_status.test.js index 958b4fb33d..35ea07522b 100644 --- a/hasura.planx.uk/tests/payment_status.test.js +++ b/hasura.planx.uk/tests/payment_status.test.js @@ -51,4 +51,19 @@ describe("payment_status", () => { expect(i).toHaveNoMutationsFor("payment_status"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query payment_status", () => { + expect(i.queries).not.toContain("payment_status"); + }); + + test("cannot create, update, or delete payment_status", () => { + expect(i).toHaveNoMutationsFor("payment_status"); + }); + }); }); diff --git a/hasura.planx.uk/tests/planning_constraints_requests.test.js b/hasura.planx.uk/tests/planning_constraints_requests.test.js index b68c951a19..2d178eb68c 100644 --- a/hasura.planx.uk/tests/planning_constraints_requests.test.js +++ b/hasura.planx.uk/tests/planning_constraints_requests.test.js @@ -44,4 +44,19 @@ describe("planning_constraints_requests", () => { expect(i).toHaveNoMutationsFor("planning_constraints_requests"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query planning_constraints_requests", () => { + expect(i.queries).not.toContain("planning_constraints_requests"); + }); + + test("cannot create, update, or delete planning_constraints_requests", () => { + expect(i).toHaveNoMutationsFor("planning_constraints_requests"); + }); + }); }); diff --git a/hasura.planx.uk/tests/scripts/buildJWT.js b/hasura.planx.uk/tests/scripts/buildJWT.js new file mode 100644 index 0000000000..755cc9ad1d --- /dev/null +++ b/hasura.planx.uk/tests/scripts/buildJWT.js @@ -0,0 +1,17 @@ +const { buildJWTForRole } = require("../utils.js"); + +/** + * @description + * Build a signed JWT for the provided role and userId. + * Useful for testing permissions in the Hasura console at a finer-grained level than the introspection tests currently allow. + * @usage pnpm build-jwt + * @example pnpm build-jwt teamEditor 123 + * @returns A JWT which can be copy/pasted to the Hasura admin console as a the "authorization" header + */ +const buildJWT = () => { + const [role, userId] = process.argv.slice(2); + const jwt = buildJWTForRole(role, userId); + console.log(`Bearer ${jwt}`) +} + +buildJWT(); diff --git a/hasura.planx.uk/tests/sessions.test.js b/hasura.planx.uk/tests/sessions.test.js index dbefe37ea5..12e708f93f 100644 --- a/hasura.planx.uk/tests/sessions.test.js +++ b/hasura.planx.uk/tests/sessions.test.js @@ -610,5 +610,20 @@ describe("sessions", () => { expect(i).toHaveNoMutationsFor("sessions"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query sessions", () => { + expect(i.queries).not.toContain("sessions"); + }); + + test("cannot create, update, or delete sessions", () => { + expect(i).toHaveNoMutationsFor("sessions"); + }); + }); }); diff --git a/hasura.planx.uk/tests/teams.test.js b/hasura.planx.uk/tests/teams.test.js index f012aab28e..6aca15d251 100644 --- a/hasura.planx.uk/tests/teams.test.js +++ b/hasura.planx.uk/tests/teams.test.js @@ -48,4 +48,27 @@ describe("teams", () => { expect(i.mutations).not.toContain("delete_teams"); }); }); + + describe("teamEditor", () => { + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("can query teams", () => { + expect(i.queries).toContain("teams"); + }); + + test("cannot update teams", () => { + expect(i.mutations).not.toContain("update_teams"); + expect(i.mutations).not.toContain("update_teams_by_pk"); + }); + + test("cannot delete teams", async () => { + expect(i.mutations).not.toContain("delete_teams"); + }); + + test("cannot insert teams", async () => { + expect(i.mutations).not.toContain("insert_teams"); + }); + }); }); diff --git a/hasura.planx.uk/tests/uniform_applications.test.js b/hasura.planx.uk/tests/uniform_applications.test.js index 7f39ff88c9..efa53fa142 100644 --- a/hasura.planx.uk/tests/uniform_applications.test.js +++ b/hasura.planx.uk/tests/uniform_applications.test.js @@ -44,4 +44,19 @@ describe("uniform_applications", () => { expect(i).toHaveNoMutationsFor("uniform_applications"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query uniform_applications", () => { + expect(i.queries).not.toContain("uniform_applications"); + }); + + test("cannot create, update, or delete uniform_applications", () => { + expect(i).toHaveNoMutationsFor("uniform_applications"); + }); + }); }); diff --git a/hasura.planx.uk/tests/users.test.js b/hasura.planx.uk/tests/users.test.js index f120240071..5c6b68b3e9 100644 --- a/hasura.planx.uk/tests/users.test.js +++ b/hasura.planx.uk/tests/users.test.js @@ -29,7 +29,6 @@ describe("users", () => { expect(i.mutations).toContain("delete_users"); }); }); - describe("platformAdmin", () => { let i; beforeAll(async () => { @@ -44,4 +43,19 @@ describe("users", () => { expect(i).toHaveNoMutationsFor("users"); }); }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("can query users", async () => { + expect(i.queries).toContain("users"); + }); + + test("cannot create, update, or delete users", async () => { + expect(i).toHaveNoMutationsFor("users"); + }); + }); }); diff --git a/hasura.planx.uk/tests/utils.js b/hasura.planx.uk/tests/utils.js index 5b189d9411..c4d724d987 100644 --- a/hasura.planx.uk/tests/utils.js +++ b/hasura.planx.uk/tests/utils.js @@ -58,7 +58,6 @@ function buildJWTForRole(role, userId = 1) { const hasura = { "x-hasura-allowed-roles": [role], "x-hasura-default-role": role, - "x-hasura-role": role, "x-hasura-user-id": userId.toString(), }; @@ -83,6 +82,7 @@ const introspectAs = async (role, userId = undefined) => { admin: gqlAdmin, public: gqlPublic, platformAdmin: gqlWithRole("platformAdmin", userId), + teamEditor: gqlWithRole("teamEditor", userId), }[role] const INTROSPECTION_QUERY = ` query IntrospectionQuery { @@ -117,4 +117,5 @@ module.exports = { gqlAdmin, gqlPublic, introspectAs, + buildJWTForRole, };