Skip to content

Commit

Permalink
feat: Automatically grant users access to the "Templates" team (#2291)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed Oct 13, 2023
1 parent b15ccbd commit 6e3e56e
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 13 deletions.
14 changes: 14 additions & 0 deletions e2e/tests/api-driven/src/hasuraTriggers/hasuraTriggers.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Feature: Database triggers

@regression @add-user-trigger
Scenario: Adding a user to Planx - with Templates team
Given the Templates team exists
When a new user is added
Then they are granted access to the Templates team
And have the teamEditor role

@regression @add-user-trigger
Scenario: Adding a user to Planx - without Templates team
Given the Templates team does not exist
When a new user is added
Then they are not granted access to the Templates team
6 changes: 6 additions & 0 deletions e2e/tests/api-driven/src/hasuraTriggers/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { $admin } from "../client";

export const cleanup = async () => {
await $admin.user._destroyAll();
await $admin.team._destroyAll();
};
62 changes: 62 additions & 0 deletions e2e/tests/api-driven/src/hasuraTriggers/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { After, Given, Then, When, World } from "@cucumber/cucumber";
import { cleanup } from "./helpers";
import { User } from "@opensystemslab/planx-core/types";
import { $admin } from "../client";
import assert from "assert";
import { createTeam, createUser } from "../globalHelpers";

export class CustomWorld extends World {
user!: User;
templatesTeamId!: number;
}

After("@add-user-trigger", async function () {
await cleanup();
});

Given("the Templates team exists", async function (this) {
const templatesTeamId = await createTeam({ slug: "templates" });

assert.ok(templatesTeamId, "Templates team is not defined");

this.templatesTeamId = templatesTeamId;
});

Given("the Templates team does not exist", async function (this) {
const templatesTeam = await $admin.team.getBySlug("templates");

assert.equal(
templatesTeam,
undefined,
"Templates team exists but should not be defined",
);
});

When<CustomWorld>("a new user is added", async function (this) {
const userId = await createUser();
const user = await $admin.user.getById(userId);

assert.ok(user, "User is not defined");

this.user = user;
});

Then<CustomWorld>(
"they are granted access to the Templates team",
async function (this) {
assert.strictEqual(this.user.teams.length, 1);
assert.strictEqual(this.user.teams[0].team.slug, "templates");
assert.strictEqual(this.user.teams[0].team.id, this.templatesTeamId);
},
);

Then<CustomWorld>("have the teamEditor role", async function (this) {
assert.strictEqual(this.user.teams[0].role, "teamEditor");
});

Then<CustomWorld>(
"they are not granted access to the Templates team",
async function (this) {
assert.strictEqual(this.user.teams.length, 0);
},
);
5 changes: 1 addition & 4 deletions editor.planx.uk/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -482,10 +482,7 @@ const EditorToolbar: React.FC<{
<ListItemText>
{user.isPlatformAdmin
? `All teams`
: user.teams
.map((team) => team.team.name)
.concat(["Templates"])
.join(", ")}
: user.teams.map((team) => team.team.name).join(", ")}
</ListItemText>
</MenuItem>
)}
Expand Down
101 changes: 101 additions & 0 deletions editor.planx.uk/src/pages/FlowEditor/lib/__tests__/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { User } from "@opensystemslab/planx-core/types";

import { FullStore, vanillaStore } from "../store";

const { getState, setState } = vanillaStore;
const { canUserEditTeam } = getState();

const redUser: User = {
id: 1,
isPlatformAdmin: false,
firstName: "Red",
lastName: "Reddison",
email: "[email protected]",
teams: [
{
role: "teamEditor",
team: {
name: "Red Team",
slug: "red-team",
id: 1,
},
},
{
role: "teamViewer",
team: {
name: "Blue Team",
slug: "blue-team",
id: 1,
},
},
],
};

const blueUser: User = {
id: 2,
isPlatformAdmin: false,
firstName: "Blue",
lastName: "Bluey",
email: "[email protected]",
teams: [
{
role: "teamEditor",
team: {
name: "Blue Team",
slug: "blue-team",
id: 1,
},
},
],
};

const readOnlyUser: User = {
id: 3,
isPlatformAdmin: false,
firstName: "Read",
lastName: "Only",
email: "[email protected]",
teams: [],
};

const adminUser: User = {
id: 4,
isPlatformAdmin: true,
firstName: "Platform",
lastName: "Admin",
email: "[email protected]",
teams: [],
};

let initialState: FullStore;

beforeEach(() => {
initialState = getState();
});

afterEach(() => setState(initialState));

describe("canUserEditTeam helper function", () => {
it("returns true when a user has teamEditor permission for a team", () => {
setState({ user: redUser });
expect(canUserEditTeam("red-team")).toBe(true);
expect(canUserEditTeam("blue-team")).toBe(false);
});

it("returns false when a user does not have permission for a team", () => {
setState({ user: blueUser });
expect(canUserEditTeam("red-team")).toBe(false);
});

it("returns false when a user does not have any permissions", () => {
setState({ user: readOnlyUser });
expect(canUserEditTeam("red-team")).toBe(false);
expect(canUserEditTeam("blue-team")).toBe(false);
});

it("returns true when a user is has the platformAdmin role", () => {
setState({ user: adminUser });
expect(canUserEditTeam("red-team")).toBe(true);
expect(canUserEditTeam("blue-team")).toBe(true);
});
});
15 changes: 6 additions & 9 deletions editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User } from "@opensystemslab/planx-core/types";
import { User, UserTeams } from "@opensystemslab/planx-core/types";
import { _client } from "client";
import jwtDecode from "jwt-decode";
import { Team } from "types";
Expand All @@ -22,16 +22,13 @@ export const userStore: StateCreator<UserStore, [], [], UserStore> = (
getUser: () => get().user,

canUserEditTeam(teamSlug) {
const user = this.getUser();
const user = get().getUser();
if (!user) return false;

return (
user.isPlatformAdmin ||
teamSlug === "templates" ||
user.teams.filter(
(team) => team.role === "teamEditor" && team.team.slug === teamSlug,
).length > 0
);
const hasTeamEditorRole = (team: UserTeams) =>
team.role === "teamEditor" && team.team.slug === teamSlug;

return user.isPlatformAdmin || user.teams.some(hasTeamEditorRole);
},

async initUserStore(jwt: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP FUNCTION IF EXISTS grant_new_user_template_team_access;
DROP TRIGGER grant_new_user_template_team_access on users;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
CREATE OR REPLACE FUNCTION grant_new_user_template_team_access() RETURNS trigger AS $$
DECLARE
templates_team_id INT;
BEGIN
SELECT id INTO templates_team_id FROM teams WHERE slug = 'templates';
IF templates_team_id IS NOT NULL THEN
INSERT INTO team_members (user_id, team_id, role) VALUES (NEW.id, templates_team_id, 'teamEditor');
END IF;

RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER grant_new_user_template_team_access AFTER INSERT ON users
FOR EACH ROW EXECUTE PROCEDURE grant_new_user_template_team_access();

COMMENT ON TRIGGER grant_new_user_template_team_access ON users
IS 'Automatically grant all new users teamEditor access to the shared Templates team';

-- Insert a record to team_members for all existing users
INSERT INTO
team_members (user_id, team_id, role)
SELECT
id,
29,
'teamEditor'
FROM
users;

0 comments on commit 6e3e56e

Please sign in to comment.