From d8a1c4b0da459e1bf911c2d88852ed9681673d18 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Tue, 6 Oct 2026 22:06:10 -0400 Subject: [PATCH] [rooms] fix subrooms permission gate --- apps/rooms/src/rooms.app.ts | 14 ++++++++------ apps/rooms/src/test/integration/api.test.ts | 15 ++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/rooms/src/rooms.app.ts b/apps/rooms/src/rooms.app.ts index 7005051..6f6ab73 100644 --- a/apps/rooms/src/rooms.app.ts +++ b/apps/rooms/src/rooms.app.ts @@ -4063,9 +4063,11 @@ const app = new Hono() // body is a JSON ARRAY of the entries to change, keyed by (Permission, Role): `Override` // is the client's checkbox, so true stores the entry for that pair and false clears it // back to the default. The stored table then overwrites the matching defaults in - // `GET /photon_access_token`. Auth-gated (401) and creator-only (403), like the other - // subroom mutations. Answers an EMPTY 200 — the client fires this and re-reads nothing, - // so there is no envelope to match. + // `GET /photon_access_token`. Auth-gated (401) and gated to the room's MANAGERS — its + // creator or a co-owner (403), the same `canManageRoom` the other room-admin writes + // take; a creator-only check here locked out co-owners the client shows the screen to, + // and an owner whose room names them in `Roles` rather than `CreatorAccountId`. Answers + // an EMPTY 200 — the client fires this and re-reads nothing, so there is no envelope. .put( '/rooms/:roomId{[0-9]+}/subrooms/:subRoomId{[0-9]+}/permissions', describeRoute({ @@ -4090,8 +4092,8 @@ const app = new Hono() 'pair the defaults don’t carry (e.g. `CAN_INVITE`) is served too. The overrides apply', 'to the subroom the caller is standing in, resolved from presence.', '', - 'Creator-only — co-owners may build in a room but not decide what a role may do.', - 'The response body is EMPTY: the client doesn’t read one.', + 'Gated to the room’s managers — its creator or a co-owner (`canManageRoom`), as the', + 'other room-admin writes are. The response body is EMPTY: the client doesn’t read one.', ].join('\n'), security: AUTHED, parameters: [roomIdParam, subRoomIdParam], @@ -4113,7 +4115,7 @@ const app = new Hono() // Scoped through the room so a subroom id from another room can't be written. const room = await getRoomById(c.env.DB, roomId) if (!room || !findSubRoom(room, subRoomId)) return c.notFound() - if (room.CreatorAccountId !== accountId) return c.body(null, 403) + if (!canManageRoom(room, accountId)) return c.body(null, 403) const permissions = parseRoomPermissions(await c.req.json().catch(() => null)) await setSubRoomPermissions(c.env.DB, subRoomId, permissions) diff --git a/apps/rooms/src/test/integration/api.test.ts b/apps/rooms/src/test/integration/api.test.ts index 22c9ebe..754ea6b 100644 --- a/apps/rooms/src/test/integration/api.test.ts +++ b/apps/rooms/src/test/integration/api.test.ts @@ -4818,19 +4818,24 @@ describe('rooms endpoints', () => { .run() } - it('is auth-gated and creator-only', async () => { + it('is auth-gated and open to the room’s managers only', async () => { const body = [ { Permission: 'CAN_SAVE_INVENTIONS', Role: 30, Override: false, Type: 0, Value: 'True' }, ] // No token → 401. expect((await putPermissions('/rooms/2/subrooms/2/permissions', body)).status).toBe(401) - // A valid token that isn't the room's creator → 403. + // A valid token that isn't the room's creator or a co-owner → 403. expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '999')).status).toBe( 403 ) - // Not even a co-owner: account 2 holds Role 30 on the seeded rooms. Co-owners may - // build in a room but don't decide what a role may do. - expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '2')).status).toBe(403) + // A co-owner may: account 2 holds Role 30 on the seeded rooms. (This body clears a + // pair that was never stored, so it changes nothing.) + expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '2')).status).toBe(200) + // A Host (a lower tier) may not. + await setRoles() + expect((await putPermissions('/rooms/2/subrooms/2/permissions', body, '748')).status).toBe( + 403 + ) // Unknown room / unknown subroom → 404. expect((await putPermissions('/rooms/99999/subrooms/2/permissions', body, '1')).status).toBe( 404