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: Automatically grant users access to the "Templates" team #2291

Merged
merged 5 commits into from
Oct 12, 2023
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
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;
DafyddLlyr marked this conversation as resolved.
Show resolved Hide resolved
Loading