From 223df3990cd2328ccf2ef8ab51c7a1b0ff9bd92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Tue, 29 Oct 2024 15:36:13 +0000 Subject: [PATCH] feat: Add check for demoUser role on login --- api.planx.uk/modules/auth/service.test.ts | 66 ++++++++++++++++++----- api.planx.uk/modules/auth/service.ts | 29 ++++++---- api.planx.uk/modules/auth/types.ts | 14 +++++ 3 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 api.planx.uk/modules/auth/types.ts diff --git a/api.planx.uk/modules/auth/service.test.ts b/api.planx.uk/modules/auth/service.test.ts index bf0f3162c9..fbe1cb3f76 100644 --- a/api.planx.uk/modules/auth/service.test.ts +++ b/api.planx.uk/modules/auth/service.test.ts @@ -1,7 +1,17 @@ +import type { User } from "@opensystemslab/planx-core/types"; import { checkUserCanAccessEnv } from "./service.js"; const mockIsStagingOnly = vi.fn(); +const mockUser: User = { + firstName: "Bilbo", + lastName: "Baggins", + id: 123, + email: "test@example.com", + isPlatformAdmin: false, + teams: [], +}; + vi.mock("../../client", () => { return { $api: { @@ -17,25 +27,22 @@ describe("canUserAccessEnv() helper function", () => { beforeAll(() => mockIsStagingOnly.mockResolvedValue(true)); test("can't access production", async () => { - const result = await checkUserCanAccessEnv( - "test@example.com", - "production", - ); + const result = await checkUserCanAccessEnv(mockUser, "production"); expect(result).toBe(false); }); test("can access staging", async () => { - const result = await checkUserCanAccessEnv("test@example.com", "staging"); + const result = await checkUserCanAccessEnv(mockUser, "staging"); expect(result).toBe(true); }); test("can access pizzas", async () => { - const result = await checkUserCanAccessEnv("test@example.com", "pizza"); + const result = await checkUserCanAccessEnv(mockUser, "pizza"); expect(result).toBe(true); }); test("can access test envs", async () => { - const result = await checkUserCanAccessEnv("test@example.com", "test"); + const result = await checkUserCanAccessEnv(mockUser, "test"); expect(result).toBe(true); }); }); @@ -44,25 +51,56 @@ describe("canUserAccessEnv() helper function", () => { beforeAll(() => mockIsStagingOnly.mockResolvedValue(false)); test("can access production", async () => { - const result = await checkUserCanAccessEnv( - "test@example.com", - "production", - ); + const result = await checkUserCanAccessEnv(mockUser, "production"); + expect(result).toBe(true); + }); + + test("can access staging", async () => { + const result = await checkUserCanAccessEnv(mockUser, "staging"); + expect(result).toBe(true); + }); + + test("can access pizzas", async () => { + const result = await checkUserCanAccessEnv(mockUser, "pizza"); + expect(result).toBe(true); + }); + + test("can access test envs", async () => { + const result = await checkUserCanAccessEnv(mockUser, "test"); expect(result).toBe(true); }); + }); + + describe("a demo user", () => { + beforeAll(() => { + mockIsStagingOnly.mockResolvedValue(false); + mockUser.teams.push({ + role: "demoUser", + team: { + name: "Demo", + slug: "demo", + id: 123, + }, + }); + }); + + test("can't access production", async () => { + const result = await checkUserCanAccessEnv(mockUser, "production"); + expect(result).toBe(false); + }); test("can access staging", async () => { - const result = await checkUserCanAccessEnv("test@example.com", "staging"); + const result = await checkUserCanAccessEnv(mockUser, "staging"); expect(result).toBe(true); }); test("can access pizzas", async () => { - const result = await checkUserCanAccessEnv("test@example.com", "pizza"); + const result = await checkUserCanAccessEnv(mockUser, "pizza"); expect(result).toBe(true); }); test("can access test envs", async () => { - const result = await checkUserCanAccessEnv("test@example.com", "test"); + const result = await checkUserCanAccessEnv(mockUser, "test"); expect(result).toBe(true); }); }); diff --git a/api.planx.uk/modules/auth/service.ts b/api.planx.uk/modules/auth/service.ts index 7e7ef1db6c..931fdfa7ca 100644 --- a/api.planx.uk/modules/auth/service.ts +++ b/api.planx.uk/modules/auth/service.ts @@ -1,13 +1,16 @@ import jwt from "jsonwebtoken"; import { $api } from "../../client/index.js"; import type { User, Role } from "@opensystemslab/planx-core/types"; +import type { HasuraClaims, JWTData } from "./types.js"; export const buildJWT = async (email: string): Promise => { - await checkUserCanAccessEnv(email, process.env.NODE_ENV); const user = await $api.user.getByEmail(email); if (!user) return; - const data = { + const hasAccess = await checkUserCanAccessEnv(user, process.env.NODE_ENV); + if (!hasAccess) return; + + const data: JWTData = { sub: user.id.toString(), email, "https://hasura.io/jwt/claims": generateHasuraClaimsForUser(user), @@ -27,7 +30,7 @@ export const buildJWTForAPIRole = () => process.env.JWT_SECRET!, ); -const generateHasuraClaimsForUser = (user: User) => ({ +const generateHasuraClaimsForUser = (user: User): HasuraClaims => ({ "x-hasura-allowed-roles": getAllowedRolesForUser(user), "x-hasura-default-role": getDefaultRoleForUser(user), "x-hasura-user-id": user.id.toString(), @@ -60,15 +63,19 @@ const getDefaultRoleForUser = (user: User): Role => { return user.isPlatformAdmin ? "platformAdmin" : "teamEditor"; }; -/** - * A staging-only user cannot access production, but can access all other envs - */ export const checkUserCanAccessEnv = async ( - email: string, + user: User, env?: string, ): Promise => { - const isStagingOnlyUser = await $api.user.isStagingOnly(email); - const isProductionEnv = env === "production"; - const userCanAccessEnv = !(isProductionEnv && isStagingOnlyUser); - return userCanAccessEnv; + // All users can access non-production environments + const isProduction = env === "production"; + if (!isProduction) return true; + + const isDemoUser = getAllowedRolesForUser(user).includes("demoUser"); + if (isDemoUser) return false; + + const isStagingOnlyUser = await $api.user.isStagingOnly(user.email); + if (isStagingOnlyUser) return false; + + return true; }; diff --git a/api.planx.uk/modules/auth/types.ts b/api.planx.uk/modules/auth/types.ts new file mode 100644 index 0000000000..74495a2aa6 --- /dev/null +++ b/api.planx.uk/modules/auth/types.ts @@ -0,0 +1,14 @@ +import type { Role } from "@opensystemslab/planx-core/types"; + +export type HasuraNamespace = "https://hasura.io/jwt/claims"; +export type HasuraClaims = { + "x-hasura-allowed-roles": Role[]; + "x-hasura-default-role": Role; + "x-hasura-user-id": string; +}; +export type HasuraJWT = Record; + +export type JWTData = HasuraJWT & { + sub: string; + email: string; +};