Repository navigation
Commit 9939854
fix(showcase): the contributor set's row-level policies apply to every holder of the set (#21104)
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 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 the
showcase contributor set's owner-isolation policies now apply to every
holder of the set, whichever way the set is held: through the
`contributor` position binding, or as a direct grant (the showcase's
delegated admin may hand this set out). A comment beside the policies
records why `positions` is 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 own `isDefault` baseline
and grants the app's own `showcase_contributor` twice. One persona gets
it directly: a `sys_user_permission_set` row and no position. The other
gets it through the `contributor` position: a `sys_user_position` row
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/explain` principal and an rls layer verdict of
`narrows`. Then, for **both** holders:
- the list equals exactly the invoices the persona owns;
- a foreign invoice by id answers 404 `RECORD_NOT_FOUND`, and so does a
foreign line by id;
- a line under an owned invoice can be created and patched, while a
foreign line PATCH answers 403 `PERMISSION_DENIED` and the stored row is
unchanged;
- re-owning a foreign invoice answers 403 `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`**:
the `showcase_task` create payload now sets `assignee` to the acting
persona, in the same way the file already sets `owner` on invoices. This
file's contributor persona holds the set by a direct grant, so
`task_own_rows` now governs it, as it governs every holder. A task
created with no assignee was invisible to the persona that created it.
That turned the `showcase_contributor x showcase_task` EDIT 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.json` is 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-showcase` and
`@objectstack/dogfood` are both `private: true`, so this diff publishes
nothing.
## Verification (merged HEAD `12ed6b654`, after merging `origin/main`)
- Dogfood pins, one run: the isolation pin,
`showcase-crud-persona-matrix`, `showcase-private-owd`,
`showcase-invoice-cbp`, `controlled-by-parent`,
`showcase-permission-zoo` and `showcase-expand-crud-gate`. Result: 7
files / 94 tests passed, exit 0.
- `pnpm --filter @objectstack/example-showcase validate`: exit 0.
Showcase `typecheck`: exit 0. Showcase `vitest run`: 29 files / 387
tests passed. Dogfood `typecheck`: exit 0. `tsc --listFiles` for that
program includes both edited test files and `permission-sets.ts`.
- `objectstack verify --rls` (showcase): exit 0. All personas: 38
proven, 0 holes. The `contributor` position persona proves
`showcase_invoice`, `showcase_invoice_line` and `showcase_task`
consistent.
- Reverse verification: `scripts/ablation-replace.mjs` put the three
`positions` lists 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 and `git diff HEAD` is empty. The
un-mutated pin is 12/12 green.
- `node scripts/pm/dispatch-gates.mjs --commands` on `12ed6b654`: all 55
derived commands were run, and `--ran` reconciles 55/55. 54 exited 0. 1
is NOT MEASURED: `check-plugin-teardown-shape.mjs --self-test` exited 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 json` on the 3 changed files: 3
files linted, 0 errors, 0 warnings, and none ignored.
`eslint.config.mjs` enables 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.json` describes
`invoice_own_rows` as carrying `positions: ['contributor']` in two
places, in the prose of an `expect` step and in a `source` line. After
this PR that parenthetical is stale. The file is outside this card's
file surface. Noted here, not filed. Carrier: none.
- `objectstack verify --rls` builds 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.
- No lint looks at a `positions` list 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 enforces `positions`
exactly as the spec documents. Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MRdbfpy4sQT8bUjmMhxsN7)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 53ed3d1 commit 9939854
3 files changed
Lines changed: 238 additions & 96 deletions
File tree
- examples/app-showcase/src/security
- packages/qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
72 | 80 | | |
73 | 81 | | |
74 | 82 | | |
| |||
77 | 85 | | |
78 | 86 | | |
79 | 87 | | |
80 | | - | |
81 | 88 | | |
82 | 89 | | |
83 | 90 | | |
| |||
92 | 99 | | |
93 | 100 | | |
94 | 101 | | |
95 | | - | |
96 | 102 | | |
97 | 103 | | |
98 | 104 | | |
| |||
108 | 114 | | |
109 | 115 | | |
110 | 116 | | |
111 | | - | |
112 | 117 | | |
113 | 118 | | |
114 | 119 | | |
| |||
Lines changed: 5 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
176 | | - | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
177 | 180 | | |
178 | 181 | | |
179 | 182 | | |
| |||
222 | 225 | | |
223 | 226 | | |
224 | 227 | | |
225 | | - | |
| 228 | + | |
226 | 229 | | |
227 | 230 | | |
228 | 231 | | |
| |||
0 commit comments