From 188e36253367ff1cedeaa9b900f511c05267c79a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:55:03 +0000 Subject: [PATCH] Fix: Allow workspace Viewers with collection Admin membership to edit templates Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> --- server/policies/team.test.ts | 2 +- server/policies/team.ts | 1 - server/routes/api/templates/templates.test.ts | 59 +++++++++++ .../routes/api/templates/test_issue.test.ts | 99 ------------------- 4 files changed, 60 insertions(+), 101 deletions(-) delete mode 100644 server/routes/api/templates/test_issue.test.ts diff --git a/server/policies/team.test.ts b/server/policies/team.test.ts index 5dedef7fc4..f682c9be4b 100644 --- a/server/policies/team.test.ts +++ b/server/policies/team.test.ts @@ -57,7 +57,7 @@ describe("policies/team", () => { const permissions = new Map([ [UserRole.Admin, true], [UserRole.Member, true], - [UserRole.Viewer, false], + [UserRole.Viewer, true], [UserRole.Guest, false], ]); for (const [role, permission] of permissions.entries()) { diff --git a/server/policies/team.ts b/server/policies/team.ts index a99b8f8d27..0b5ec8867d 100644 --- a/server/policies/team.ts +++ b/server/policies/team.ts @@ -15,7 +15,6 @@ allow(User, "readTemplate", Team, (actor, team) => and( // !actor.isGuest, - !actor.isViewer, isTeamModel(actor, team) ) ); diff --git a/server/routes/api/templates/templates.test.ts b/server/routes/api/templates/templates.test.ts index e6f3190177..d9d8a680bd 100644 --- a/server/routes/api/templates/templates.test.ts +++ b/server/routes/api/templates/templates.test.ts @@ -1,9 +1,12 @@ import { buildAdmin, buildUser, + buildViewer, buildTemplate, buildCollection, } from "@server/test/factories"; +import { UserMembership } from "@server/models"; +import { CollectionPermission } from "@shared/types"; import { getTestServer } from "@server/test/support"; const server = getTestServer(); @@ -159,6 +162,62 @@ describe("#templates.update", () => { expect(body.data.data).toEqual(data); }); + it("should allow workspace viewer with collection admin membership to update template", async () => { + const viewer = await buildViewer(); + const collection = await buildCollection({ + teamId: viewer.teamId, + permission: null, + }); + + await UserMembership.create({ + createdById: viewer.id, + collectionId: collection.id, + userId: viewer.id, + permission: CollectionPermission.Admin, + }); + + const template = await buildTemplate({ + teamId: viewer.teamId, + collectionId: collection.id, + }); + + const res = await server.post("/api/templates.update", { + body: { + token: viewer.getJwtToken(), + id: template.id, + title: "Updated by collection manager", + }, + }); + + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data.title).toEqual("Updated by collection manager"); + expect(body.policies[0].abilities.update).toEqual(true); + }); + + it("should not allow workspace viewer without collection admin to update template", async () => { + const viewer = await buildViewer(); + const collection = await buildCollection({ + teamId: viewer.teamId, + permission: CollectionPermission.ReadWrite, + }); + + const template = await buildTemplate({ + teamId: viewer.teamId, + collectionId: collection.id, + }); + + const res = await server.post("/api/templates.update", { + body: { + token: viewer.getJwtToken(), + id: template.id, + title: "Should fail", + }, + }); + + expect(res.status).toEqual(403); + }); + it("should allow admin to move template to another accessible collection", async () => { const admin = await buildAdmin(); const template = await buildTemplate({ diff --git a/server/routes/api/templates/test_issue.test.ts b/server/routes/api/templates/test_issue.test.ts deleted file mode 100644 index 61b28921a2..0000000000 --- a/server/routes/api/templates/test_issue.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { UserMembership } from "@server/models"; -import { - buildViewer, - buildCollection, -} from "@server/test/factories"; -import { CollectionPermission } from "@shared/types"; -import { getTestServer } from "@server/test/support"; - -const server = getTestServer(); - -describe("viewer with collection admin can edit templates", () => { - it("should return update:true policy after viewer creates a template in their managed collection", async () => { - // Create a workspace viewer - const viewer = await buildViewer(); - - // Create a collection with permission: null (private/restricted) - const collection = await buildCollection({ - teamId: viewer.teamId, - permission: null, - }); - - // Give the viewer Admin (Manager) collection membership - await UserMembership.create({ - createdById: viewer.id, - collectionId: collection.id, - userId: viewer.id, - permission: CollectionPermission.Admin, - }); - - // Create a template via the API (as the viewer) - const createRes = await server.post("/api/templates.create", { - body: { - token: viewer.getJwtToken(), - collectionId: collection.id, - title: "My Template", - }, - }); - - const createBody = await createRes.json(); - console.log("Create Status:", createRes.status); - console.log("Create Policies:", JSON.stringify(createBody.policies, null, 2)); - - expect(createRes.status).toEqual(200); - - const templateId = createBody.data.id; - const policy = createBody.policies?.find((p: {id: string}) => p.id === templateId); - console.log("Template policy after create:", JSON.stringify(policy, null, 2)); - - // The template should be editable by the creator (viewer + collection manager) - expect(policy?.abilities?.update).toBeTruthy(); - }); - - it("should allow viewer to create a template via templates.create if they have collection admin", async () => { - const viewer = await buildViewer(); - - const collection = await buildCollection({ - teamId: viewer.teamId, - permission: null, - }); - - await UserMembership.create({ - createdById: viewer.id, - collectionId: collection.id, - userId: viewer.id, - permission: CollectionPermission.Admin, - }); - - const createRes = await server.post("/api/templates.create", { - body: { - token: viewer.getJwtToken(), - collectionId: collection.id, - title: "Viewer Template", - }, - }); - - console.log("Status:", createRes.status); - expect(createRes.status).toEqual(200); - }); - - it("should NOT allow viewer to create a template without collection admin", async () => { - const viewer = await buildViewer(); - - const collection = await buildCollection({ - teamId: viewer.teamId, - permission: CollectionPermission.ReadWrite, // public collection - }); - - const createRes = await server.post("/api/templates.create", { - body: { - token: viewer.getJwtToken(), - collectionId: collection.id, - title: "Viewer Template", - }, - }); - - console.log("Status:", createRes.status); - expect(createRes.status).toEqual(403); - }); -});