Repository navigation
Commit f46ef4d
fix(plugin-security)!: an update of a controlled_by_parent detail also asks for edit access on the master its body names (ADR-0055 §3) (#25066)
Fixes #25044
Clause-②: no (narrowing)
## What changed
ADR-0055 §3 requires edit access to the master for every write to a
`controlled_by_parent` detail, and it reads a create's master from the
master id in the incoming body. A write can involve more than one
master. For one shape of write, the master-detail write gate
(`assertControlledByParentWrite`, step 2.8) asked the master-edit
question of only one of the masters the write involves. It now asks
every one of them.
- `packages/plugins/plugin-security/src/security-plugin.ts`, inside
`assertControlledByParentWrite` only. The gate first asks about the
master or masters it already asked about, unchanged. Then it runs the
same chain walk on the master id the write's body names, unless that
master was already asked. The walk is
`assertControlledByParentChainEditable`, the one walk PR #25042
extracted; no second copy exists.
- A refusal uses the by-id leg's own envelope: `403 PERMISSION_DENIED`,
naming the caller's detail and operation, with its leg recorded. It is
thrown before the write runs, so nothing is written.
- An insert's master already is its body's, so it is asked once.
- A write that selects no row involves no master and is not refused.
- No master scope is composed into any write.
`computeControlledByParentFilter`, `packages/objectql/**` and
`packages/spec/**` are untouched.
-
`packages/plugins/plugin-security/src/controlled-by-parent-every-master-edit.test.ts`
(new): 17 cases on a real stack. That is `ObjectQL` over better-sqlite3,
the real `SecurityPlugin`, the real sharing service and middleware, and
contexts built by `resolveAuthzContext` and `assembleExecutionContext`.
- `.changeset/25044-cbp-every-master-edit.md`:
`@objectstack/plugin-security` `minor`. It is BREAKING under the
launch-window convention for an accept-set narrowing. It carries the
`Clause-②` line and the ADR-0087 disposition `not-required
(no-migration-prescription)`.
## Reach
Reach is stated abstractly here, because the disclosure ruling recorded
on #9387 applies. The reading was re-measured on a real `ObjectQL` +
`SqlDriver` + `SecurityPlugin`, for a member and for an organization
admin. The write landed on `origin/main` `eddffc3390` (before PR #25042)
and again on PR #25042's head `033fc75a09` (after it). The probes were
throwaway and are not committed. The detail is held privately.
## Verification
Every run below is on this branch's head `a447bfdb8f`, after the final
commit, unless it is marked otherwise. Each one ran under the shared
verify lock.
- **Build.** The dependency closure was built with `pnpm --filter
'@objectstack/dogfood^...' build` (a superset of
`@objectstack/plugin-security^...`, plugin-security itself included). It
exited 0.
- **The new pins.** `controlled-by-parent-every-master-edit.test.ts`
passes 17/17, beside PR #25042's
`controlled-by-parent-write-shape-master-edit.test.ts` (32/32 for the
two files together).
- **The package.** `pnpm --filter @objectstack/plugin-security exec
vitest run`: Test Files 202 passed (202), Tests 4156 passed and 45
skipped (4201). `pnpm --filter @objectstack/plugin-security typecheck`
exited 0. That covers `tsc` on both programs and `check:test-typecheck`,
and the new test file is in the `tsconfig.test.json` program
(`--listFiles`, 1 hit).
- **Dogfood battery** (`pnpm --filter @objectstack/dogfood test`, no
shard and no path, owed by the `!` title). The lock's verdict was
command-exit 0. Test Files: 285 passed, 3 skipped (288). Tests: 2375
passed, 18 skipped (2393).
- **Derived gate families.** `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` derives 69 families. All
69 ran, and each exit code was captured before any pipe. `--ran`
reports: 69 run, 0 NOT-MEASURED, 0 UNRUN.
- **Ablation**, one leg per pinned behaviour. The fix was committed
first. Each leg is a mutation made through
`scripts/ablation-replace.mjs`, where the anchor must hit and the blob
must change. Each restore is `git checkout HEAD --`, proven by blob
equality with `HEAD` and an empty `git diff HEAD`. The subject is
imported as `./security-plugin.js` from source, so no `dist/` sits on
the path. The result:
| leg | mutation | went red |
|---|---|---|
| L1 | drop the new question after the by-id leg | the refusal pins (7
cases) and the count pin |
| L2 | drop the new question after the predicate leg | the refusal pins
(7) and the count pin |
| L3 | ask the body's master instead of the stored one | the
stored-master pin and the count pin |
| L4 | a blanket refusal in place of the walk | the lands-as-it-did pins
(7), and the refusal and stored-master pins on the envelope |
| L5 | drop the no-row guard | the no-row pin |
| L6 | drop the already-asked skip | the count pin (an insert's master
asked twice) |
The first L4 attempt was refused by the tool: its replacement contained
its anchor, so the anchor count did not move. It measured nothing and
was rerun with a non-overlapping anchor. The unmutated control passed
17/17. The final tree is byte-identical to `HEAD`.
- **Base.** The branch is `main` at `b2890909aa` (PR #25042 landed) plus
this change, merged with `origin/main` `72518d20f5` before the runs
above. Since then `main` has gained one commit (`4bf079c693`,
plugin-auth), which is disjoint from this diff.
## Acceptance notes
- Two adjacent readings in the same gate family turned up while
measuring. Neither is changed here. Both go to the seat privately, for
filing under the same ruling.
- The question is asked of what the body names before the engine's own
field strips run. On a reference the engine would strip from the write,
a body naming a master the caller may not edit is therefore refused,
where before it was dropped. That fails closed. Mirroring the engine's
strip rules in the gate would make a second copy of them.
- The new walk runs on the master id the body names, as the insert's
walk already does.
---
**Seat's append** (`domain:services` seat 1, from the dev's patch-round
report `6310152347` on #25044; the dev writes its PR body once):
- Patch round 1 (`4b4cfed7af`): only the changeset headline changes,
narrowed to the update shape this diff covers, per contract review
`6310100720` ③.1. No code or test line moved.
- `git diff --stat a447bfd 4b4cfed` touches only
`.changeset/25044-cbp-every-master-edit.md` (+1/−1). Code is
byte-identical, so the dogfood battery and the package and gate runs at
`a447bfdb8f` stand.
---
_Generated by [Claude
Code](https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent c67f244 commit f46ef4d
3 files changed
Lines changed: 510 additions & 4 deletions
File tree
- .changeset
- packages/plugins/plugin-security/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
0 commit comments