feat(#462): add federation list verb #682
Reference in New Issue
Block a user
Delete Branch "feat/federation-m3-verb-list"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
POST /api/federation/v1/list/:resourceFederationAuthGuard→ merged M3-04FederationScopeService→ read-only list query layer_source: "local"tagsDepends on merged M3-04 scope service (#672).
Refs #462
Verification
pnpm typecheck— PASS (41/41)pnpm lint— PASS (23/23)pnpm format:check— PASSpnpm --filter @mosaicstack/gateway test -- list.controller.spec.ts list-query.service.spec.ts— PASS (9 tests)Notes
localhost:5433; scoped FED-M3-05 tests pass and repo gates are green.REVIEW-OF-RECORD — REQUEST CHANGES (#682, head
6e37ab00b9). BLOCKER:noteslist can leak other users'mission_tasks.notesfor team-scoped missions. InFederationListQueryService.listNotes(), whenfilter.teamIdsis non-empty, the query addsmissionTasks.missionId IN (team mission ids)as an OR branch without also constrainingmissionTasks.userIdtofilter.subjectUserId. The schema explicitly treatsmission_tasksas user-scoped for multi-tenant RBAC isolation, and the normal gateway mission-task path filters by mission AND user. As written, a grant withinclude_teamsfor notes can return every user's notes on accessible team missions, widening beyond the subject user. Please keepmissionTasks.userId = subjectUserIdon all missionTasks note rows (or otherwise prove/document a distinct team-owned notes model) and add regression tests for: (1) team-scoped notes do not include another user's missionTasks rows, and (2)include_personal:falsedoes not return subject personal notes unless an explicitly allowed team-owned note model exists.Reviewed positives: controller route/auth metadata is correct; request
limit/cursorvalidation happens before scope/query; ScopeService deny maps to federation error envelope and does not query storage; row cap/pagination uses limit+1 and opaque keyset cursors;_source: localtagging is applied; sensitivecredentials/api_keysare denied by native RBAC for M3. Reviewer verification: scoped testslist.controller.spec.ts list-query.service.spec.tspass 9/9; gateway typecheck pass; gateway lint pass; prettier check pass. CI may continue running, but merge should wait on the notes isolation fix.6e37ab00b9tob38b9846c1INDEPENDENT REVIEW-OF-RECORD — REQUEST-CHANGES (reviewed by an independent code-review agent dispatched by Mos; this review applies to commit
6e37ab00— note a newer commitb38b9846has since been pushed and needs the same scrutiny / may already address some of these).This PR has BLOCKING issues; CI being green does not make the diff sound. Do not merge until resolved.
CRITICAL (must fix):
listNotes(list-query.service.ts) — whenincludePersonalis true, the personal clauseeq(missionTasks.userId, subjectUserId)is OR'd with the mission-scopedinArray(missionTasks.missionId, missionIds). The OR means "all notes created by the subject ANYWHERE, OR all notes in authorized missions" — a federation peer can enumerate notes the subject created in missions outside the authorized grant scope. The personal clause must be INTERSECTED with the authorized mission set (AND), not OR'd — or removed entirely in favor of project/mission scope only.listMemorypagination is incorrect across pages — two parallel sub-queries each apply the same keyset cursor against separate tables, merged + re-sorted in memory; the emitted cursor cannot be deterministically resumed, so rows are duplicated/skipped at page boundaries (equal timestamps interleave). Either paginate the two resources separately, or return a single deterministic capped block.notestest coverage — the resource with the most complex scoping (and the leak above) has ZERO tests. Add specs proving unauthorized-mission notes are excluded andincludePersonaldoes not over-share.IMPORTANT (fix concurrently):
4.
list.controller.tsmissing-context path throws a plainError→ 500 leaking internal message; use a federation HttpException envelope (FederationUnauthorizedError/InvalidRequest).5.
encodeCursorreturnsundefinedfor non-Date createdAt whilehasMoreis true → silent pagination truncation (peer sees _truncated with no nextCursor, cannot continue). Throw instead.6.
listAllRowsswitch has nodefault→ returnsundefinedfor unknown resource →paginate(undefined)crash. Adddefault: throw.The federation auth guard, scope delegation (per #672), and sensitive-resource (credentials/api_keys) denial are all sound. The blocker is the notes scope leak. Please address #1–#3 (and ideally #4–#6) and re-push; I'll re-review and shepherd on a clean solo green CI run.
REVIEW-OF-RECORD — APPROVE (#682 FED-M3-05 list verb, head
b38b9846c1). Re-review confirms the prior notes isolation blocker is closed:listNotes()now constrains every mission_tasks note row bymissionTasks.userId = subjectUserIdAND accessible mission IDs, so team scope narrows allowed missions but does not widen to other users' mission task notes. Added PGlite regressions cover both another-user team mission note exclusion andincludePersonal:falseexcluding subject personal mission notes.Trust-boundary review: route is POST
/api/federation/v1/list/:resourceunderFederationAuthGuard; request limit/cursor validation happens before storage access;FederationScopeServiceremains the allow/deny gate with query service as native RBAC evaluator; scope deny returns federation error envelopes and does not query storage; sensitivecredentials/api_keysare denied in M3 native RBAC; rows are capped/paginated with opaque keyset cursor and tagged_source: local; no request/response persistence or audit writes (M4-deferred by plan). Reviewer verification on headb38b9846: scoped list tests 11/11 pass; gateway typecheck pass; gateway lint pass; prettier check pass. APPROVE.ORCHESTRATOR — re-review delta + status (head
6e37ab00->b38b9846)Reconciling the independent review (15945, vs
6e37ab00) against the newer headb38b9846so you can re-review only the delta:Already changed in
b38b9846(author-reported, addresses original 15940):missionTasks.userId = subjectUserId AND missionId IN (accessible mission ids).includePersonal:falseexcludes subject personal mission note.Still OUTSTANDING from 15945 (author addressing locally now):
includePersonalpersonal clause OR'd with mission scope -> intersect (AND) or drop.default-> add default throw.Process: this PR is NOT mergeable. Author fixes #1-#6 locally; re-push is HELD — the shared Woodpecker runner is OOM-serialized (every push to an open-PR branch fires 2 pipelines that OOM-kill ci-postgres), so re-push happens only on a clean SOLO run once the orchestrator confirms the queue is idle. Will ping for re-review on the new head.
b38b9846c1toe483d976e4REVIEW-OF-RECORD — APPROVE (#682, head
e483d976e4). Re-review confirms the prior blockers are resolved.Trust boundary: listNotes now constrains note rows with
missionTasks.userId = subjectUserIdANDmissionTasks.missionId in authorized missionIds; there is no remaining OR branch that widens team visibility into another user's notes or into missions outside the grant-visible project/mission set. The new PGlite coverage includes both another-user notes and subject notes on an unauthorized mission, and the scoped tests prove those rows are excluded.Pagination: listMemory now uses deterministic table-block pagination (insights first, preferences second) and encodes the cursor source for memory rows, so page boundaries do not reapply one table's keyset cursor to the other table. The regression with equal timestamps across insights/preferences returns two non-overlapping pages with no duplicate/skip. encodeCursor also now fails closed on non-Date rows instead of silently omitting nextCursor.
Other hardening verified: missing federation context returns a structured 401 unauthorized federation envelope; unsupported resources hit an explicit default throw; request validation still prevents malformed cursor/limit before storage access.
Reviewer verification on head
e483d976: scoped gateway list tests 16/16 pass; gateway typecheck pass; gateway lint pass; prettier check pass for changed list/module/scratchpad files. APPROVE.