Repository navigation
Commit 0148071
fix(plugin-security)!: under a controlled_by_parent declaration whose one master reference is readonly or system, every write that sets it is refused (ADR-0055 §1) (#25256)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuTXH2TuPG1JBf3xdmP114Fh)_
Fixes #25095
Clause-②: no (narrowing)
Metadata-protocol: none
⛔ Disclosure ruling `5323310017` (#9387) applies: this PR states the
rule and its prescription only. No probe, posture, caller, request or
served value is recorded here or in any comment; the measurements are
held privately.
> **Route.** Ruling `6345869077` on #25095 chose route B: refuse at the
write gate. This PR implements that ruling; it supersedes the narrower
insert-only route this draft first carried.
## The rule
ADR-0055 §1 derives a `controlled_by_parent` detail's access from
exactly one required `master_detail` that every write of a detail
supplies. Since #25092 the parse refuses that master when it is declared
`readonly: true` or `system: true` (`controlledByParentMasterRefusal`,
shape `master-not-writable`). An object with that declaration still
reaches the registry through a door that does not parse:
- a raw `registerObject` or `registerApp`;
- `defineStack(…, { strict: false })`;
- a stored object the boot registers under its `[metadata_spec_invalid]`
diagnostic (the registry registers off-spec rows for availability).
On such an object the write gate let writes that set the master
reference through, and a detail could end up outside the master its
access derives from: readable by nobody, while the write reported
success.
**Now** the write gate refuses every write that sets that reference,
`422 INVALID_METADATA`, before any master is asked and before anything
is written:
- every insert, single or batch, whether or not its body names a master;
- every update whose body carries the reference key as an own property,
whatever the value, `null` included (an upsert reaches the gate as one
of the two).
The message is the parse's own refusal of the declaration, prefixed with
the write it refused, so it carries the parse's prescription: remove the
flag and declare the master reference as an ordinary required field, or
give the object its own `sharingModel`. Both flags follow one rule, and
every caller the gate judges gets the same answer.
**Premise checked first** (ruling item 7): on all three doors above, the
object registers, `resolveCbpRelation` resolves its relation, and the
flag is readable on the field def the relation came from.
## What changed
- `packages/plugins/plugin-security/src/security-plugin.ts`
- `CbpRelation.masterNotWritable`: resolved in `resolveCbpRelation` from
the same field def the relation came from, judged by
`controlledByParentMasterRefusal` on that one def, so the flags and the
sentence are spelled once, in the parse. The relation still resolves;
the read leg, the cache and `reportCbpMasterUnresolved` are untouched.
No second registry reader, and `omissionRefusedByValidation` is not
widened.
- `assertControlledByParentWrite`: right after the missing-relation leg,
before any master is asked, `writesMasterReference` (insert always;
update when the body carries the key) throws
`MasterReferenceNotWritableError`. A delete, and an update that leaves
the key alone, take the path they took before.
- The two stale freeze-note paragraphs at the empty-master branch are
rewritten to the current state (both ramp legs have landed, the parse
refuses all three shapes, and no insert of theirs reaches that branch).
The freeze itself stands.
- `packages/plugins/plugin-security/src/errors.ts`:
`MasterReferenceNotWritableError` beside
`MasterDetailRelationMissingError` (`INVALID_METADATA`, 422; the
error-code ledger already has this row for plugin-security). Not
exported. The `MasterReferenceMissingError` docblock is corrected.
- `.changeset/25095-cbp-unwritable-master-write.md`: `patch`, BREAKING,
`Clause-②: no (narrowing)`, `Metadata-protocol: none`, ADR-0087
`not-required (already-registered cbp-master-relation-tiers-retired)`,
as `pnpm check:adr-0087-registration` judges it.
- `content/docs/api/error-catalog.mdx` and
`content/docs/protocol/kernel/error-handling.mdx`: corrected, because
this PR makes them false. They said a write that leaves out a `readonly`
or `system` master reference answers `422 MISSING_REQUIRED_FIELD`; such
a write now answers `422 INVALID_METADATA` (the two-item list above),
and the `422 MISSING_REQUIRED_FIELD` exception is narrowed to a by-id
update or delete of a detail whose stored master reference is null. The
flat example body moves to that case.
## Unchanged, on purpose
- **Reads.** The relation still resolves, so rows already stored keep
the master their reads derive from.
- **A delete, and an update whose body leaves the key out.**
- System contexts (the bypass is unchanged), objects with an ordinary
required master, and every object that does not declare
`controlled_by_parent`.
- No `packages/spec`, `packages/objectql` or lint change, and no
read-side change.
- The served member (`checkControlledByParentWrite`) asks a by-id update
with no body, so it never reaches the new refusal, and its closed
outcome vocabulary is untouched.
## Tests
`packages/plugins/plugin-security/src/controlled-by-parent-unwritable-master-write.test.ts`
(renamed from `…-insert.test.ts`, 83 cases). Real stack: `ObjectQL` over
better-sqlite3, the real `SecurityPlugin`, the real sharing service and
middleware, a member context from `resolveAuthzContext` +
`assembleExecutionContext`, a platform admin's carried posture, and the
engine's security source wired as a kernel wires it, so a detail read
composes the engine's `ReadableIds` leaf over its master. The flagged
declarations are registered raw through `registerApp`, for `readonly`,
`system` and both, on an ordinary object and on a `sys_`-named one. For
a member and for a platform admin:
- a single create naming a master, a single create naming none, and a
batch create each answer `INVALID_METADATA` / 422 with exactly the
parse's message, and nothing is stored; the master walk is never
entered;
- a by-id update naming a master, and one setting it to `null`, answer
the same, and the row keeps its master;
- controls: a stored row is still read, an update that leaves the key
out lands, a delete lands;
- a detail declared as the three in-repo `controlled_by_parent` objects
declare theirs (`Field.masterDetail`, `required: true`, through the
parse) takes single and batch creates, a re-pointing update, a note
update and a delete as before.
A system create lands on every flagged declaration, and each flagged
declaration is one the parse refuses at `fields` as
`master-not-writable`.
Moved pins, by the ruling: `controlled-by-parent-sharing.test.ts`'s
`[#8688]` flagged-insert cases now expect `INVALID_METADATA` with the
parse's message (and its stale "AUTHORABLE" wording is corrected);
`controlled-by-parent-every-row-master.test.ts`'s cases on its
`system`-flagged detail now expect `INVALID_METADATA` for every batch
create and every emptying update, and its "the stored master is asked
first" ordering pin moves to the validated detail, where it still holds.
Ablation and the gate list are reported in the card's `os-dev-report`.
## Notes
- **Branch.** `claude/project-thread-fi0g2a` is the harness-pinned
branch for this session and names no issue, so the Duplicate Fix Guard's
branch-name hint will not match. The claim on #25095 (`6345497639`) is
the identity record.
- **Serial.** PR #25198 (#24932) landed first; this branch merged `main`
after it. The read scope it moved to the engine's `ReadableIds` leaf is
not this gate, and the new suite fills the engine's security-source slot
the way #25198's rigs do. Split card #25223 waits on this card.
- `packages/spec/src/conversions/refused-shapes.ts` still says a stored
`master-not-writable` object "loses the save door" only; a pointer is
left for the `domain:spec` seat (#18549), since spec is not this lane's.
- A `CONTRACT_REVIEW_TIER` review of record on the narrowing face is
owed before the queue.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01LQG7z3CxDLtiMaHLbTFSeB
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8dfd126 commit 0148071
8 files changed
Lines changed: 727 additions & 151 deletions
File tree
- .changeset
- content/docs
- api
- protocol/kernel
- 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 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
178 | 178 | | |
179 | 179 | | |
180 | 180 | | |
181 | | - | |
182 | | - | |
| 181 | + | |
| 182 | + | |
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
186 | | - | |
187 | | - | |
| 186 | + | |
| 187 | + | |
188 | 188 | | |
189 | 189 | | |
190 | 190 | | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
199 | 210 | | |
200 | 211 | | |
201 | 212 | | |
| |||
205 | 216 | | |
206 | 217 | | |
207 | 218 | | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
218 | 224 | | |
219 | | - | |
220 | | - | |
221 | | - | |
| 225 | + | |
| 226 | + | |
222 | 227 | | |
223 | 228 | | |
224 | 229 | | |
| |||
973 | 978 | | |
974 | 979 | | |
975 | 980 | | |
976 | | - | |
| 981 | + | |
977 | 982 | | |
978 | 983 | | |
979 | 984 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
281 | 281 | | |
282 | 282 | | |
283 | 283 | | |
284 | | - | |
| 284 | + | |
285 | 285 | | |
286 | 286 | | |
287 | 287 | | |
| |||
534 | 534 | | |
535 | 535 | | |
536 | 536 | | |
537 | | - | |
| 537 | + | |
538 | 538 | | |
539 | 539 | | |
540 | 540 | | |
| |||
552 | 552 | | |
553 | 553 | | |
554 | 554 | | |
555 | | - | |
| 555 | + | |
556 | 556 | | |
557 | | - | |
558 | | - | |
559 | | - | |
560 | | - | |
561 | | - | |
562 | | - | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
563 | 568 | | |
564 | 569 | | |
565 | 570 | | |
566 | 571 | | |
567 | | - | |
568 | | - | |
569 | | - | |
570 | | - | |
571 | | - | |
572 | | - | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
573 | 587 | | |
574 | 588 | | |
575 | 589 | | |
| |||
579 | 593 | | |
580 | 594 | | |
581 | 595 | | |
582 | | - | |
583 | | - | |
584 | | - | |
585 | | - | |
586 | | - | |
587 | 596 | | |
588 | 597 | | |
589 | 598 | | |
590 | | - | |
591 | | - | |
592 | | - | |
593 | | - | |
594 | | - | |
595 | | - | |
596 | | - | |
597 | | - | |
598 | | - | |
599 | | - | |
600 | | - | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
601 | 606 | | |
602 | 607 | | |
603 | 608 | | |
| |||
0 commit comments