Repository navigation
fix(showcase): the contributor set's row-level policies apply to every holder of the set - #21104
Conversation
…y holder of the set The three row-level policies on showcase_contributor (task_own_rows, invoice_own_rows, invoice_owner_immutable) no longer carry a positions list: the set itself is their applicability domain, so they hold for every holder of the set, whichever way the set is held. Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7 Co-authored-by: Claude <noreply@anthropic.com>
…contributor set for a direct holder and a position holder The pin no longer governs sign-ups with a mirror of the contributor set. It grants the app's own showcase_contributor once directly and once through the contributor position, proves each persona holds the set by that route alone, and asserts for both: list = owned invoices, foreign invoice and line by id 404 RECORD_NOT_FOUND, foreign line PATCH 403 PERMISSION_DENIED with the row unchanged, and owner re-assignment refused in both directions. Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7 Co-authored-by: Claude <noreply@anthropic.com>
…gned to the acting persona The contributor persona holds showcase_contributor by a direct grant, and the set's task_own_rows policy selects tasks by assignee == current_user.email. A task created with no assignee is invisible to its creator, which turned the contributor x showcase_task EDIT cell into a record-level refusal. The payload now assigns the row to the persona, as it already sets owner on invoices, so the cell stays a CRUD verdict. No matrix cell and no census count changes. Claude-Session: https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7 Co-authored-by: Claude <noreply@anthropic.com>
…owcase-contributor-owner-rls
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
Landing note: this is a permission-boundary change (human floor), so the PR stayed draft until the maintainer decided. The maintainer authorized the landing in Claude Code session Generated by Claude Code |
Fixes #21052
Clause-②: no
Showcase authoring only. No platform, runtime or spec code changes. Landing stays with the maintainer: this is a permission-boundary change, so the PR stays draft for the maintainer's review.
What changed
examples/app-showcase/src/security/permission-sets.ts: the three row-level policies onshowcase_contributor(task_own_rows,invoice_own_rows,invoice_owner_immutable) no longer carry apositionslist. The set itself is their applicability domain, so the showcase contributor set's owner-isolation policies now apply to every holder of the set, whichever way the set is held: through thecontributorposition binding, or as a direct grant (the showcase's delegated admin may hand this set out). A comment beside the policies records whypositionsis not used there. No other set in the file authors row-level policies.packages/qa/dogfood/test/showcase-invoice-seed-isolation.dogfood.test.ts: before this PR the pin governed sign-ups with a mirror of the contributor set that it declared itself, so it never ran the shipped set. It now boots the showcase with the app's ownisDefaultbaseline and grants the app's ownshowcase_contributortwice. One persona gets it directly: asys_user_permission_setrow and no position. The other gets it through thecontributorposition: asys_user_positionrow plus the app's position-to-set binding, and no direct row. A premise case proves each persona holds the set by its route alone, from the rows, the/security/explainprincipal and an rls layer verdict ofnarrows. Then, for both holders:RECORD_NOT_FOUND, and so does a foreign line by id;PERMISSION_DENIEDand the stored row is unchanged;PERMISSION_DENIED, and so does reassigning an owned invoice to someone else. The stored owner is unchanged in both cases.packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts: theshowcase_taskcreate payload now setsassigneeto the acting persona, in the same way the file already setsowneron invoices. This file's contributor persona holds the set by a direct grant, sotask_own_rowsnow governs it, as it governs every holder. A task created with no assignee was invisible to the persona that created it. That turned theshowcase_contributor x showcase_taskEDIT cell into a record-level 403 instead of a CRUD verdict, measured red on the first run after the fix. With the payload fixed, the cell is a CRUD verdict again.access-matrix.jsonis untouched. The census stays at 87 allow / 77 deny with 9 baseline flips, so no matrix cell changes side.No changeset is needed:
@objectstack/example-showcaseand@objectstack/dogfoodare bothprivate: true, so this diff publishes nothing.Verification (merged HEAD
12ed6b654, after mergingorigin/main)showcase-crud-persona-matrix,showcase-private-owd,showcase-invoice-cbp,controlled-by-parent,showcase-permission-zooandshowcase-expand-crud-gate. Result: 7 files / 94 tests passed, exit 0.pnpm --filter @objectstack/example-showcase validate: exit 0. Showcasetypecheck: exit 0. Showcasevitest run: 29 files / 387 tests passed. Dogfoodtypecheck: exit 0.tsc --listFilesfor that program includes both edited test files andpermission-sets.ts.objectstack verify --rls(showcase): exit 0. All personas: 38 proven, 0 holes. Thecontributorposition persona provesshowcase_invoice,showcase_invoice_lineandshowcase_taskconsistent.scripts/ablation-replace.mjsput the threepositionslists back (anchor hit x3, blob change proven on disk) and the isolation pin went red, 6 failed / 6 passed of 12. The tool then restored the file: blob equals HEAD andgit diff HEADis empty. The un-mutated pin is 12/12 green.node scripts/pm/dispatch-gates.mjs --commandson12ed6b654: all 55 derived commands were run, and--ranreconciles 55/55. 54 exited 0. 1 is NOT MEASURED:check-plugin-teardown-shape.mjs --self-testexited 3 because its pinned positive-control commit is unreachable from this shallow clone. That is a checker-health battery and does not depend on this diff.eslint --no-inline-config --format jsonon the 3 changed files: 3 files linted, 0 errors, 0 warnings, and none ignored.eslint.config.mjsenables no type-aware linting, so this diff cannot change the verdict on any file it does not touch.Acceptance notes
docs/qa/platform-checklist/areas/search.jsondescribesinvoice_own_rowsas carryingpositions: ['contributor']in two places, in the prose of anexpectstep and in asourceline. After this PR that parenthetical is stale. The file is outside this card's file surface. Noted here, not filed. Carrier: none.objectstack verify --rlsbuilds its personas from the app's declared positions plus a base persona that holds no app set. It never builds a persona that holds an app permission set by direct grant, so that way of holding a set is outside what its green proves. Noted, not filed: it is a gap in what the verifier covers, not a defect. Carrier: none.positionslist on the row-level policy of a set that is not a baseline (isDefault) set. Such a lint could help authors. It is not filed because the runtime enforcespositionsexactly as the spec documents. Carrier: none.Generated by Claude Code