From 4daa9af530e9898e29d3bda7230cda7e7c0c364f Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 6 Aug 2026 14:15:41 +0000 Subject: [PATCH] fix(security): gate board-key collections Apply live owner grant checks across every effective company for collection routes and encode next-request revocation behavior. Co-Authored-By: Paperclip --- .../security/board-key-authorization.test.ts | 29 +++++++++++++++- .../src/security/board-key-route-registry.ts | 34 +++++++++++++++---- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/server/src/security/board-key-authorization.test.ts b/server/src/security/board-key-authorization.test.ts index a9785a422a..60d04176da 100644 --- a/server/src/security/board-key-authorization.test.ts +++ b/server/src/security/board-key-authorization.test.ts @@ -65,7 +65,7 @@ function createLiveAuthorityDb() { : table === instanceUserRoles ? (state.instanceAdmin ? [{ id: randomUUID() }] : []) : table === principalPermissionGrants - ? [...state.permissionGrants].map((permissionKey) => ({ permissionKey })) + ? [...state.permissionGrants].map((permissionKey) => ({ companyId, permissionKey })) : []; return { where: () => Promise.resolve(rows), @@ -261,4 +261,31 @@ describe("board-key effective authority", () => { metadata, ), 403); }); + + it("applies owner permission-grant revocation to company collections on the next request", async () => { + const { db, state, companyId } = createLiveAuthorityDb(); + const authentication = await boardAuthService(db).authenticateBoardApiKey(TOKEN); + const req = requestFor(authentication); + const metadata = { + ...lookupBoardKeyRoute("GET", "/api/companies"), + action: "agents:write" as const, + }; + + await expect(authorizeBoardKey( + db, + req, + metadata.action, + async () => ({ companyId: null, resourceType: "company_collection", resourceId: null }), + metadata, + )).resolves.toBeUndefined(); + + state.permissionGrants.delete("agents:configure"); + await expectDenied(authorizeBoardKey( + db, + req, + metadata.action, + async () => ({ companyId: null, resourceType: "company_collection", resourceId: null }), + metadata, + ), 403); + }); }); diff --git a/server/src/security/board-key-route-registry.ts b/server/src/security/board-key-route-registry.ts index 8083725651..d8759ba64d 100644 --- a/server/src/security/board-key-route-registry.ts +++ b/server/src/security/board-key-route-registry.ts @@ -585,22 +585,33 @@ const OWNER_GRANT_REQUIREMENTS: Partial row.permissionKey)); - return permissionKeys.every((permissionKey) => liveKeys.has(permissionKey)); + const liveKeysByCompany = new Map>(); + for (const row of rows) { + const liveKeys = liveKeysByCompany.get(row.companyId) ?? new Set(); + liveKeys.add(row.permissionKey); + liveKeysByCompany.set(row.companyId, liveKeys); + } + return companyIds.every((companyId) => { + const liveKeys = liveKeysByCompany.get(companyId); + return permissionKeys.every((permissionKey) => liveKeys?.has(permissionKey)); + }); } async function auditDecision( @@ -689,9 +700,18 @@ export async function authorizeBoardKey( await denyBoardKey(db, req, metadata, resource, "owner_instance_admin_missing", "forbidden"); } } else if (metadata.classification === "company_collection") { - if ((req.actor.companyIds ?? []).length === 0) { + const companyIds = req.actor.companyIds ?? []; + if (companyIds.length === 0) { await denyBoardKey(db, req, metadata, resource, "owner_company_membership_missing", "forbidden"); } + if (!await ownerHasRequiredGrant( + db, + req.actor.boardKeyOwnerId!, + companyIds, + action as BoardPermissionKey, + )) { + await denyBoardKey(db, req, metadata, resource, "owner_permission_grant_missing", "forbidden"); + } } else if (metadata.resolver === "downstream_company") { // A family may be inventoried before it has a safe generic resolver. It is // available to migrated legacy keys through existing route-level checks, @@ -710,7 +730,7 @@ export async function authorizeBoardKey( if (!await ownerHasRequiredGrant( db, req.actor.boardKeyOwnerId!, - resource.companyId, + [resource.companyId], action as BoardPermissionKey, )) { await denyBoardKey(db, req, metadata, resource, "owner_permission_grant_missing", "forbidden");