Skip to content

Commit

Permalink
feat: teamEditor role (#2182)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Sep 11, 2023
1 parent 7dcf60c commit dc46e96
Show file tree
Hide file tree
Showing 37 changed files with 551 additions and 94 deletions.
65 changes: 40 additions & 25 deletions api.planx.uk/modules/auth/service.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> => {
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";
};
2 changes: 2 additions & 0 deletions api.planx.uk/modules/auth/strategy/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/modules/team/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
}),
});

Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/modules/team/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ components:
example: 123
role:
type: string
enum: ["teamViewer", "teamAdmin"]
enum: ["teamViewer", "teamEditor"]
paths:
/team/{teamId}/add-member:
put:
Expand Down
16 changes: 8 additions & 8 deletions api.planx.uk/modules/team/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -80,7 +80,7 @@ describe("Adding a user to a team", () => {
.set(authHeader())
.send({
userId: 123,
role: "teamAdmin",
role: "teamEditor",
})
.expect(500)
.then((res) => {
Expand All @@ -98,7 +98,7 @@ describe("Adding a user to a team", () => {
.set(authHeader())
.send({
userId: 123,
role: "teamAdmin",
role: "teamEditor",
})
.expect(200)
.then((res) => {
Expand Down Expand Up @@ -156,7 +156,7 @@ describe("Removing a user from a team", () => {
.set(authHeader())
.send({
userId: 123,
role: "teamAdmin",
role: "teamEditor",
})
.expect(200)
.then((res) => {
Expand All @@ -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);
});
Expand All @@ -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) => {
Expand Down Expand Up @@ -229,7 +229,7 @@ describe("Changing a user's role", () => {
.set(authHeader())
.send({
userId: 123,
role: "teamAdmin",
role: "teamEditor",
})
.expect(500)
.then((res) => {
Expand All @@ -247,7 +247,7 @@ describe("Changing a user's role", () => {
.set(authHeader())
.send({
userId: 123,
role: "teamAdmin",
role: "teamEditor",
})
.expect(200)
.then((res) => {
Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions api.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions api.planx.uk/tests/mockJWT.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
};
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/api-driven/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions e2e/tests/api-driven/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion e2e/tests/ui-driven/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions e2e/tests/ui-driven/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions e2e/tests/ui-driven/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
},
},
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/ui-driven/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
};
Expand Down
2 changes: 1 addition & 1 deletion editor.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions editor.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions editor.planx.uk/src/lib/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ApolloClient,
createHttpLink,
DefaultContext,
from,
InMemoryCache,
} from "@apollo/client";
Expand Down Expand Up @@ -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"
}
}
Loading

0 comments on commit dc46e96

Please sign in to comment.