Skip to content

Commit 325944b

Browse files
docs(adr): ADR-0066 D2 ⑧ note — a super-user set votes on every field of its object (#29459)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9A6kAUSDc7TXDVUJeUigpMx)_ Fixes #29106. This is the card's second PR: the ADR note for the runtime change that landed in #29387. It is governed (`docs/adr/**`, Tier H) and stays a draft for a governed approver. Clause-②: no Metadata-protocol: none Ruling-ref: 6573563853 ## 维护者速读 - 改了什么:只在 ADR-0066 D2 ⑧ 下追加一条 2026-11-02 的说明,记录裁决 6573563853 选 A:持「查看全部 / 修改全部」的权限集在字段合并中对该对象每个字段投票。Status 行不动。 - 为什么改:运行时改动已由 #29387 合入,ADR 需要如实记录现行规则。 - 风险与回滚:纯文档,无运行时影响;回滚 = revert 本 PR。 - 需要你做的:治理审批人(GOVERNED_APPROVERS)审阅并批准;本 PR 保持草稿,席位不会自行转为就绪。 ## What changes - `docs/adr/0066-unified-authorization-model.md`: one append-only note under D2 ⑧, after the 2026-10-28 note. It records that a set holding `viewAllRecords` on an object votes `readable: true` on every field of it, and that `modifyAllRecords` votes `readable: true` and `editable: true`. View All never grants an edit. Who holds the bypass is the row-level answer, read through one function (`holdsSuperuserBypass`, `@objectstack/core`). Union without deny is otherwise unchanged. The note also names the one consequence: a set can no longer withhold a field from its own View All / Modify All holders. - The Status line is untouched. Seat 2 ruled "note only", because earlier ⑧ notes (2026-10-26, 2026-10-28) left it alone. - No changeset: a docs-only change publishes nothing. ## Why The ruling of record 6573563853 (letter A) amended ⑧'s field-merge posture. #29387 shipped the runtime half, and this records it where the posture is written. ## Verification - Branch restarted from `main` at `aca58cdb6`, which includes #29387 (`4138ce862`). The diff is 1 file, +2 lines. - `node scripts/pm/dispatch-gates.mjs --commands` on this diff derived 21 families: `check-adr-links` (+ `--self-test`), `check:adr-anchors`, `check:adr-index`, `check:adr-symbol-anchors`, `check-closing-keyword-parity` (+ `--self-test`), `check:doc-authoring`, `check:pm-governed-merges`, `check:pm-prior-rulings`, `check:closed-ceremony-text` and the rest. All 21 exit 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ADHkmSgjH5Vi4jT3KVJpvt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 85ea12f commit 325944b

1 file changed

Lines changed: 2 additions & 0 deletions

File tree

‎docs/adr/0066-unified-authorization-model.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ Captured for the record; **out of scope for Phases 1–4 above**. Each is anchor
138138
> **Note (2026-10-26) — a field permission entry may carry a row condition (#25866, ruling of record 6408558941).** `FieldPermissionSchema.when` is a row predicate in the same canonical CEL as `rowLevelSecurity[].using`, on the **edit** vote only (`when` beside `readable: false` is refused at parse). The condition **narrows where an entry votes**: on a row that matches, the entry votes as declared; on a row that does not, the entry is **absent**, as if the set had not declared the field. It does **not** change the posture recorded here — field grants still **union without deny**: a matched conditioned lock is out-voted by a sibling's unconditioned `editable: true` exactly as before, an entry without `when` votes on every row, and only declaring entries vote. The write gate judges the stored row before an update and the incoming row of an insert, in process (`RLSCompiler.compileRowPredicate` + `storedFormCheckJudge`); a row that cannot be judged (a predicate update, a stored row the caller cannot read, a condition that does not resolve for the request) and every row-blind reader (`/auth/me/permissions`, the field projections, `os explain`) apply the entry as its lock, so no reading is wider than the gate's. The condition is validated like an RLS predicate at authoring (`rls-predicate-*` findings on `permissions[].fields[].when`). Taking an edit away that another set grants remains the job of the muting/deny layer ⑦ still owes.
139139

140140
> **Note (2026-10-28) — a third reader of `when` holds the row: the record-scoped field projection (#26594, ruling of record 6457467702, letter B).** `GET /api/v1/data/:object/:id/field-access` (`SecurityPlugin.getRecordFieldAccess`, the answer a record's edit form reads) judges each field permission's `when` against the **stored** row with the write gate's own function, `computeForbiddenFieldWrites`, asked as an update by id of that row naming every field over the same sets, posture and delegator intersection the gate takes — so its `editable` list is the gate's field-level verdict for that row, a conditioned lock disables a field only on the rows its condition names, and no second evaluator of `when` exists for it to drift from. The row-blind readers (`/auth/me/permissions`, the object-level field projections, `os explain`) are unchanged and still apply a conditioned entry as its lock, and `readable` stays row-blind. The record is read first exactly as `GET /api/v1/data/:object/:id` reads it, so an absent or unreadable row answers that read's refusal, and no reading is wider than the gate's.
141+
142+
> **Note (2026-11-02) — a super-user set votes on every field of its object (#29106, ruling of record 6573563853, letter A).** In the field-permission merge, a permission set that holds the super-user bypass on an object now votes on **every** field of that object: `viewAllRecords` votes `readable: true`, and `modifyAllRecords` votes `readable: true` and `editable: true`. View All never grants an edit. Who holds the bypass is the row-level answer: the set's own entry for the object if it has one, otherwise its `'*'` wildcard. One function answers it, `holdsSuperuserBypass` (`packages/core/src/security/effective-object-permissions.ts`). `PermissionEvaluator.superuserBypassSets` reads it directly, and both field merges, `getFieldPermissions` and the row-blind `/auth/me/permissions` merge (`buildEffectiveFieldPermissions`), read it through `superuserFieldVote`. So every reader of the merge answers the same: the list projection, record read, the sort and filter gate, the field-level write gate, `/auth/me/permissions` and `os explain`. Before this, a set that granted the object through its super-user bits but declared no field entry cast no vote, so an app set's `readable: false` hid the field from a platform admin who also held the app set. Union without deny is otherwise unchanged. A caller who holds no super-user set on the object is still narrowed by a declared mask, and a field's `requiredPermissions` capability gate still applies to everyone. One consequence: a set can no longer withhold a field from its own View All / Modify All holders. To withhold a field, grant the object without those bits. Taking a field away from a super-user remains the job of the muting/deny layer ⑦ still owes.
141143
- **⑨ Authoring-time validation for capability references** *(recorded 2026-07 pre-launch architecture assessment)*. `systemPermissions` / `requiredPermissions` are free strings today; a typo (`mange_users`) fails closed at runtime — the safe direction — but is undiscoverable: nothing reports that the referenced capability exists nowhere. When D1 lands the `sys_permission` registry, add the authoring/publish-gate lint alongside it: referencing an unregistered capability warns at author time (consistent with ADR-0049 honesty and the contract-first Prime Directive — reject at the producer, don't tolerate at the consumer). **Landed (2026-07):** `validateCapabilityReferences` in `@objectstack/lint` warns (never errors — cross-package capabilities and the fail-closed runtime make a hard gate wrong) when a `requiredPermissions` reference resolves against neither the built-in set (`@objectstack/spec` `PLATFORM_CAPABILITY_NAMES`), nor a capability the stack grants via a permission set's `systemPermissions`, nor a `sys_capability` seed row. Wired into `os validate` and `os lint`. `systemPermissions` (the declaration side) is deliberately not flagged.
142144

143145
> **Note (2026-10-14) — ⑨'s seed-row source is gone.** With `sys_capability` retired by #15204

0 commit comments

Comments
 (0)