Mirrored from GitHub github.com/roostorg/coop

fix(mrt): scope queue mutations to the correct org (#872) master

* fix(mrt): org-scope accessible-queue mutations (GHSA-mf74-gf5j-hxr9) addAccessibleQueuesForUser / removeAccessibleQueuesForUser accepted an arbitrary queueId / userId with no org-scoping, so an authenticated user in Org A could grant (or revoke) access to queues and users belonging to Org B. Thread the caller's orgId through the resolver, service wrapper, and QueueOperations write layer, and reject (403 AccessibleQueueNotInOrgError) if any target queue or user does not belong to the caller's org. Regression tests cover both IDOR dimensions (cross-org queueId and cross-org userId) for both add and remove, plus a same-org happy path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(mrt): use transactional test harness, trim comments - Switch the accessible-queue IDOR tests to makeTransactionalTestWithFixture (per-org rollback, no manual cleanup). - Drop GHSA references and "what was" narration from code comments; keep only minimal present-tense notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mrt): assert original user keeps access after cross-org revoke attempt Strengthen the GHSA-mf74 regression test for removeAccessibleQueuesForUser: after a rejected cross-org revoke, also assert the attacker still sees their own queue — confirming the rejected call left the original access intact. Co-Authored-By: pi * fix(mrt): org-scope create/update queue userIds (GHSA-mf74-gf5j-hxr9) createManualReviewQueue and updateManualReviewQueue accepted un-scoped userIds and wrote them straight into users_and_accessible_queues, the same un-scoped-input shape fixed in add/removeAccessibleQueuesForUser. create always creates the queue in the caller's org (orgId from the invoker) but granted access to users in other orgs; update's queue-row write was org-scoped but the join-table writes ran un-scoped inside the transaction. Refactor assertUsersAndQueuesInOrg into composable assertQueuesInOrg + assertUsersInOrg (add/remove callers unchanged), then guard create with assertUsersInOrg and update with assertUsersAndQueuesInOrg before any write, rejecting AccessibleQueueNotInOrgError for cross-org targets. Defense-in-depth/integrity fix: reads (getReviewableQueuesForUser, getQueueForOrg) and the dequeue path (checkQueueExists) are already org-scoped by the invoker, so a cross-org user could not actually see or act on the queue -- no confidentiality impact. Closes the same un-scoped-input shape the advisory fixed elsewhere. Regression tests cover cross-org userId for both create and update, asserting the mutation rejects and that update leaves the victim user absent from the queue's viewers. Co-Authored-By: pi * test(mrt): use a real org user in JobRouting queue fixtures The org-scoping guard added to createManualReviewQueue rejects userIds that are not members of the caller's org. JobRouting.test.ts invented userId = uid() and never created a matching public.users row, so every queue fixture in that suite threw AccessibleQueueNotInOrgError. Create the user via the createUser fixture helper (real org member) and wire its cleanup, matching QueueOperations.test.ts and CommentOperations.test.ts. Co-Authored-By: pi --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>


+331 -26
6 changed files