diff --git a/e2e/tests/api-driven/src/demo-workspace/demoUser.feature b/e2e/tests/api-driven/src/demo-workspace/demoUser.feature new file mode 100644 index 0000000000..fc59e23b9b --- /dev/null +++ b/e2e/tests/api-driven/src/demo-workspace/demoUser.feature @@ -0,0 +1,94 @@ + Feature: Demo user access +# We create the steps data from these Given statements below +# altering these will alter what data is created for testing + Background: + + Given I have the following teams in the database: + | id | name | slug | + | 1 | Open Systems Lab | open-systems-lab | + | 29 | Templates | templates | + | 30 | Open Digital Planning | open-digital-planning | + | 32 | Demo | demo | + | 45 | Other Team | other-team | + + And I have two users in the database: + | id | first_name | last_name | email | + | 1 | Demo | User | demo.user@email.com | + | 2 | Nota | Demo | nota.demo@email.com | + + And I have the following flows in the database: + | creator_id | name | slug | team_id | + | 1 | Demo Flow | demo-flow | 32 | + | 2 | Test OSL | test-osl | 1 | + | 2 | Test Templates | test-templates | 29 | + | 2 | Test ODP | test-odp | 30 | + | 2 | Other Flow | other-flow | 45 | + And I am a demoUser + + @demo-user-permissions + Scenario Outline: I can only view my own flows + When I am in the "" team + Then I should only see my own flows + But I should not see flows that I have not created + Examples: + | TEAM | + | demo | + + + @demo-user-permissions + Scenario Outline: I can only view specific teams + When I query the teams table + Then I can access the teams with slug: "" + But I should not be able to access the Other Team + + Examples: + | SLUG | + | open-systems-lab | + | templates | + | open-digital-planning | + | demo | + + @demo-user-permissions + Scenario Outline: Creating a new flow + When I insert a flow into the team: "" + Then I should not be able to create a flow + But I should be able to create a flow in the Demo team + + Examples: + | TEAM | + | templates | + | open-systems-lab | + | open-digital-planning | + + @demo-user-permissions + Scenario Outline: Actioning my own flows + When I am on my own flow + Then I should be able to "" the flow + + Examples: + | ACTION | + | update | + | delete | + + @demo-user-permissions + Scenario Outline: Actioning flows in other teams + When I am in the "" team + Then I should be able to see a flow + But I should not have access to modify the flow + + Examples: + | TEAM | + | templates | + | open-systems-lab | + | open-digital-planning | + + @demo-user-permissions + Scenario Outline: Editing team settings + When I am in the "" team + Then I should not have access to team settings + + Examples: + | TEAM | + | templates | + | open-systems-lab | + | open-digital-planning | \ No newline at end of file diff --git a/e2e/tests/api-driven/src/demo-workspace/helper.ts b/e2e/tests/api-driven/src/demo-workspace/helper.ts new file mode 100644 index 0000000000..e3a203e035 --- /dev/null +++ b/e2e/tests/api-driven/src/demo-workspace/helper.ts @@ -0,0 +1,320 @@ +import gql from "graphql-tag"; +import { $admin } from "../client"; +import { Team, User } from "@opensystemslab/planx-core/types"; +import { UUID } from "crypto"; + +export type Flow = { + creator_id?: number; + slug: string; + id: UUID; +}; + +export type TeamsAndFlows = { + id: number; + slug: string; + flows: Flow[]; +}; +export type FlowArgs = { teamId: number; slug: string; name: string }; + +export type DataTableRecord = Record; +export type DataTableArray = Record[]; + +export const cleanup = async () => { + await $admin.flow._destroyAll(); + await $admin.team._destroyAll(); + await $admin.user._destroyAll(); +}; + +export const checkTeamsExist = async ( + teamArray: DataTableArray, +): Promise => { + const existenceArray: Team[] = []; + for (const team of teamArray) { + const teamObj = await $admin.team.getBySlug(team.slug); + existenceArray.push(teamObj); + } + return existenceArray; +}; + +export async function createTeam(newTeam: DataTableRecord): Promise { + try { + const response: { insert_teams_one: { id: number } } = + await $admin.client.request( + gql` + mutation CreateTeam($name: String!, $id: Int, $slug: String!) { + insert_teams_one( + object: { + id: $id + name: $name + slug: $slug + team_settings: { data: {} } + integrations: { data: {} } + } + ) { + id + } + } + `, + { + ...newTeam, + }, + ); + return response.insert_teams_one.id; + } catch (error) { + return 0; + } +} + +export const createTeamFromArray = async (array: DataTableArray) => { + const teamIdArray = await Promise.all( + array.map(async (team) => { + const id = await createTeam(team); + return id; + }), + ); + return teamIdArray; +}; + +export const createFlowFromArray = async ( + client, + flow: DataTableRecord, +): Promise => { + try { + const flowId = await createFlow(client, { + teamId: Number(flow.team_id), + name: flow.name, + slug: flow.slug, + }); + return flowId; + } catch (error) { + console.error(`Error adding flow ${flow.slug}`, error); + return false; + } +}; + +export const createFlow = async ( + client, + args: FlowArgs, +): Promise => { + const { flow } = await client.request( + gql` + mutation InsertFlow($teamId: Int!, $slug: String!, $name: String!) { + flow: insert_flows_one( + object: { team_id: $teamId, slug: $slug, name: $name } + ) { + id + slug + creator_id + } + } + `, + { + teamId: args.teamId, + slug: args.slug, + name: args.name, + }, + ); + + return flow; +}; + +export async function createUser(args: Omit): Promise { + const response: { insert_users_one: { id: number } } = + await $admin.client.request( + gql` + mutation CreateUser( + $id: Int + $first_name: String! + $last_name: String! + $email: String! + $is_platform_admin: Boolean + ) { + insert_users_one( + object: { + id: $id + first_name: $first_name + last_name: $last_name + email: $email + is_platform_admin: $is_platform_admin + } + ) { + id + } + } + `, + { + id: args.id, + first_name: args.firstName, + last_name: args.lastName, + email: args.email, + is_platform_admin: args.isPlatformAdmin, + }, + ); + return response.insert_users_one.id; +} + +export async function createDemoUser( + args: Omit, +): Promise { + const response: { insert_users_one: { id: number } } = + await $admin.client.request( + gql` + mutation CreateUser( + $id: Int + $first_name: String! + $last_name: String! + $email: String! + $is_platform_admin: Boolean = false + $role: user_roles_enum! + ) { + insert_users_one( + object: { + id: $id + first_name: $first_name + last_name: $last_name + email: $email + is_platform_admin: $is_platform_admin + teams: { data: { role: $role, team_id: 32 } } + } + ) { + id + } + } + `, + { + id: args.id, + first_name: args.firstName, + last_name: args.lastName, + email: args.email, + is_platform_admin: args.isPlatformAdmin, + role: "demoUser", + }, + ); + return response.insert_users_one.id; +} + +export async function getTeams(client): Promise { + const { teams } = await client.request(gql` + query getTeams { + teams { + id + slug + team_settings { + homepage + } + } + } + `); + + return teams; +} + +export async function getTeamAndFlowsBySlug( + client, + slug: string, +): Promise { + const { + teams: [team], + } = await client.request( + gql` + query getTeamAndFlowsBySlug($slug: String) { + teams(where: { slug: { _eq: $slug } }) { + id + slug + flows { + creator_id + slug + id + } + } + } + `, + { + slug, + }, + ); + + return team; +} + +export async function getFlowBySlug(client, slug: string): Promise { + const { + flows: [flow], + } = await client.request( + gql` + query getFlowBySlug($slug: String) { + flows(where: { slug: { _eq: $slug } }) { + slug + id + } + } + `, + { + slug: slug, + }, + ); + + return flow; +} + +export async function updateFlow(client, flowId: UUID): Promise { + const { update_flows_by_pk: response } = await client.request( + gql` + mutation updateFlow($flowId: uuid!) { + result: update_flows_by_pk( + pk_columns: { id: $flowId } + _set: { slug: "new-slug", name: "new Name" } + ) { + id + } + } + `, + + { flowId: flowId }, + ); + return response; +} + +export async function deleteFlow(client, flowId: UUID): Promise { + const { delete_flows_by_pk: response } = await client.request( + gql` + mutation deleteFlow($flowId: uuid!) { + delete_flows_by_pk(id: $flowId) { + id + } + } + `, + + { flowId: flowId }, + ); + return response; +} +export async function updateTeamSettings(client, teamId: number) { + try { + const { update_team_settings: response } = await client.request( + gql` + mutation updateTeamSettings($teamId: Int!) { + update_team_settings( + where: { team_id: { _eq: $teamId } } + _set: { + homepage: "newpage.com" + submission_email: "new.email@email.com" + } + ) { + returning { + team_id + submission_email + homepage + } + } + } + `, + { + teamId: teamId, + }, + ); + return response; + } catch (error) { + return false; + } +} diff --git a/e2e/tests/api-driven/src/demo-workspace/steps/background_steps.ts b/e2e/tests/api-driven/src/demo-workspace/steps/background_steps.ts new file mode 100644 index 0000000000..ae33e477b6 --- /dev/null +++ b/e2e/tests/api-driven/src/demo-workspace/steps/background_steps.ts @@ -0,0 +1,134 @@ +import { After, Before, DataTable, Given, World } from "@cucumber/cucumber"; +import { + checkTeamsExist, + cleanup, + createDemoUser, + createFlowFromArray, + createTeamFromArray, + createUser, + DataTableArray, + DataTableRecord, + Flow, +} from "../helper"; +import { $admin, getClient } from "../../client"; +import { strict as assert } from "node:assert"; +import { CoreDomainClient } from "@opensystemslab/planx-core"; +import { Team, User } from "@opensystemslab/planx-core/types"; + +export class CustomWorld extends World { + demoClient!: CoreDomainClient["client"]; + adminClient!: CoreDomainClient["client"]; + demoUser!: Omit; + otherTeam!: DataTableRecord; + insertFlowTeamId!: number; + demoFlowSlug!: string; + adminFlowSlug!: string; + demoTeamsArray!: Team[]; + currentTeamId!: number; + teamFlows!: Flow[]; +} + +Before("@demo-user-permissions", async function () { + await cleanup(); +}); + +After("@demo-user-permissions", async function () { + await cleanup(); +}); + +Given( + "I have two users in the database:", + async function (dataTable: DataTable) { + const featureData: DataTableArray = dataTable.hashes(); + + const userOneId = await createDemoUser({ + id: Number(featureData[0].id), + firstName: featureData[0].first_name, + lastName: featureData[0].last_name, + email: featureData[0].email, + isPlatformAdmin: false, + }); + + const userTwoId = await createUser({ + id: Number(featureData[1].id), + firstName: featureData[1].first_name, + lastName: featureData[1].last_name, + email: featureData[1].email, + isPlatformAdmin: true, + }); + + const userOne = await $admin.user.getById(userOneId); + const userTwo = await $admin.user.getById(userTwoId); + + if (userOne) { + const { client: demoClient } = await getClient(userOne?.email); + this.demoClient = demoClient; + this.demoUser = userOne; + } + if (userTwo) { + const { client: adminClient } = await getClient(userTwo?.email); + this.adminClient = adminClient; + } + + assert.ok(userOne && userTwo, "Users have not been added correctly"); + }, +); + +Given( + "I have the following teams in the database:", + async function (this, dataTable: DataTable) { + const teamsArray: DataTableArray = dataTable.hashes(); + + await createTeamFromArray(teamsArray); + + const getTeamsArray = await checkTeamsExist(teamsArray); + const otherTeam = teamsArray.find((team) => team.slug === "other-team"); + if (otherTeam) this.otherTeam = otherTeam; + + assert.ok(getTeamsArray, "Teams have not been added correctly"); + assert.equal( + getTeamsArray.length, + teamsArray.length, + "Not all teams have been added", + ); + }, +); + +Given( + "I have the following flows in the database:", + async function (this, dataTable: DataTable) { + const flowsArray = dataTable.hashes(); + const expectedFlowLength = flowsArray.length; + + const demoFlow = flowsArray.find((flow) => flow.creator_id === "1"); + if (demoFlow) this.demoFlowSlug = demoFlow.slug; + + const newFlowArray: (Flow | false)[] = []; + + for (const flow of flowsArray) { + let result: Flow | boolean = false; + if (flow.creator_id === "1") { + result = await createFlowFromArray(this.demoClient, flow); + } + if (flow.creator_id === "2") { + result = await createFlowFromArray(this.adminClient, flow); + } + newFlowArray.push(result); + } + + assert.equal( + newFlowArray.length, + expectedFlowLength, + "Not all the flows have been added", + ); + assert.ok( + newFlowArray.every((result) => result !== false), + "Flows have not been added successfully", + ); + }, +); + +Given("I am a demoUser", async function () { + const userOne = await $admin.user.getById(1); + assert.ok(userOne?.teams[0].role === "demoUser", "User 1 is a demo user"); +}); diff --git a/e2e/tests/api-driven/src/demo-workspace/steps/navigation_steps.ts b/e2e/tests/api-driven/src/demo-workspace/steps/navigation_steps.ts new file mode 100644 index 0000000000..dbc9248802 --- /dev/null +++ b/e2e/tests/api-driven/src/demo-workspace/steps/navigation_steps.ts @@ -0,0 +1,48 @@ +import { When } from "@cucumber/cucumber"; +import { $admin } from "../../client"; +import { strict as assert } from "node:assert"; +import { getFlowBySlug, getTeamAndFlowsBySlug, getTeams } from "../helper"; +import { CustomWorld } from "./background_steps"; + +When("I am in the {string} team", async function (this, teamSlug) { + const team = await getTeamAndFlowsBySlug(this.demoClient, teamSlug); + this.currentTeamId = team.id; + this.teamFlows = team.flows; + if (teamSlug !== "demo") { + this.adminFlowSlug = team.flows[0].slug; + } else { + this.demoFlowSlug = team.flows[0].slug; + } + assert.equal(teamSlug, team.slug, "Error retrieving the correct team"); +}); + +When("I query the teams table", async function (this) { + const teamsArray = await getTeams(this.demoClient); + this.demoTeamsArray = teamsArray; + assert.ok(teamsArray, "Teams not fetched correctly"); +}); + +When( + "I insert a flow into the team: {string}", + async function (this, teamSlug) { + const team = await $admin.team.getBySlug(teamSlug); + this.insertFlowTeamId = team.id; + }, +); + +When("I am on my own flow", async function (this) { + const flow = await getFlowBySlug(this.demoClient, this.demoFlowSlug); + assert.equal(flow.slug, this.demoFlowSlug, "Incorrect flow has been fetched"); +}); + +When( + "I want to edit a flow that I did not create", + async function (this) { + const flow = await getFlowBySlug(this.adminClient, this.adminFlowSlug); + assert.equal( + flow.slug, + this.adminFlowSlug, + "Incorrect flow has been fetched", + ); + }, +); diff --git a/e2e/tests/api-driven/src/demo-workspace/steps/verification_steps.ts b/e2e/tests/api-driven/src/demo-workspace/steps/verification_steps.ts new file mode 100644 index 0000000000..d24f727f46 --- /dev/null +++ b/e2e/tests/api-driven/src/demo-workspace/steps/verification_steps.ts @@ -0,0 +1,120 @@ +import { Then } from "@cucumber/cucumber"; +import { CustomWorld } from "./background_steps"; +import { strict as assert } from "node:assert"; +import { + createFlow, + deleteFlow, + Flow, + getFlowBySlug, + updateFlow, + updateTeamSettings, +} from "../helper"; + +Then("I should only see my own flows", async function (this) { + assert.equal( + this.demoUser.id, + this.teamFlows[0].creator_id, + "Creator ID is incorrect", + ); +}); + +Then( + "I should not see flows that I have not created", + async function (this) { + const doesContainOtherFlows = this.teamFlows.find( + (flow) => flow.creator_id !== this.demoUser.id, + ); + assert.ok(!doesContainOtherFlows, "Other flows are in the array"); + }, +); + +Then( + "I can access the teams with slug: {string}", + async function (this, teamSlug) { + const canAccessTeam = this.demoTeamsArray.find( + (team) => team.slug === teamSlug, + ); + assert.ok(canAccessTeam, "Team is not in the array"); + }, +); + +Then( + "I should not be able to access the Other Team", + async function (this) { + const cannotAccessOtherTeam = this.demoTeamsArray.find( + (team) => team.slug !== this.otherTeam?.slug, + ); + assert.ok(cannotAccessOtherTeam, "Other Team is in the array"); + }, +); + +Then( + "I should not be able to create a flow", + async function (this) { + let hasSucceeded: Flow | false; + try { + hasSucceeded = await createFlow(this.demoClient, { + name: "Bad flow", + slug: "bad-flow", + teamId: this.insertFlowTeamId, + }); + } catch (error) { + hasSucceeded = false; + } + assert.ok(!hasSucceeded, "Flow was able to be created on this team"); + }, +); + +Then("I should be able to create a flow in the Demo team", async function () { + const hasSucceeded = await createFlow(this.demoClient, { + name: "Good flow", + slug: "good-flow", + teamId: 32, + }); + assert.ok(hasSucceeded, "Flow not added correctly"); +}); + +Then("I should be able to see a flow", async function (this) { + const flow = await getFlowBySlug(this.demoClient, this.adminFlowSlug); + assert.equal( + flow.slug, + this.adminFlowSlug, + "Incorrect flow has been fetched", + ); +}); + +Then( + "I should be able to {string} the flow", + async function (this, action) { + const demoFlow = await getFlowBySlug(this.demoClient, this.demoFlowSlug); + const hasSucceeded = + (await action) === "update" + ? updateFlow(this.demoClient, demoFlow.id) + : deleteFlow(this.demoClient, demoFlow.id); + + assert.ok(hasSucceeded, `Cannot ${action} the flow `); + }, +); + +Then( + "I should not have access to modify the flow", + async function (this) { + const canUpdate = await updateFlow(this.demoClient, this.teamFlows[0].id); + const canDelete = await deleteFlow(this.demoClient, this.teamFlows[0].id); + assert.ok( + !canUpdate && !canDelete, + "Flow can be modified by the demo user", + ); + }, +); + +Then( + "I should not have access to team settings", + async function (this) { + const canUpdateSettings = await updateTeamSettings( + this.demoClient, + this.currentTeamId, + ); + assert.ok(!canUpdateSettings, "Demo User can update the team settings"); + }, +); diff --git a/e2e/tests/api-driven/src/jwt.ts b/e2e/tests/api-driven/src/jwt.ts index 27abe0e0f2..3b80d47860 100644 --- a/e2e/tests/api-driven/src/jwt.ts +++ b/e2e/tests/api-driven/src/jwt.ts @@ -31,7 +31,6 @@ 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"); @@ -47,5 +46,10 @@ const getAllowedRolesForUser = (user: User): Role[] => { * This is the role of least privilege for the user */ const getDefaultRoleForUser = (user: User): Role => { - return user.isPlatformAdmin ? "platformAdmin" : "teamEditor"; + const teamRoles = user.teams.map((teamRole) => teamRole.role); + const isDemoUser = teamRoles.includes("demoUser"); + if (user.isPlatformAdmin) return "platformAdmin"; + if (isDemoUser) return "demoUser"; + + return "teamEditor"; };