Repository navigation
feat(spec,security): a field permission entry can carry a row condition (when) on its edit vote (#25866) - #25980
Conversation
…on (`when`) on its edit vote (#25866) Ruling of record 6408558941, letter A. `FieldPermissionSchema.when` is a row condition in the same canonical CEL as `rowLevelSecurity[].using`, on the edit vote only: `readable: false` beside it is refused at parse. A conditioned entry votes as declared on the rows the condition names and is absent elsewhere, so the most-permissive merge and the declaring-sets-vote rule are unchanged (ADR-0066 ⑧, note of 2026-10-26). - spec: the key, its refine, the aliases, the describe; generated artefacts, the dropped-refinements ledger and the liveness ledger. - core: `fieldPermissionVote` (one vote per verdict) and the row-blind `buildEffectiveFieldPermissions` behind /auth/me/permissions. - plugin-security: `RLSCompiler.compileRowPredicate` (one predicate, the `check` clause's compile, guard and faces; `userContextOf` extracted from `compileFilter`); `getFieldPermissions(object, sets, judge?)`; the write gate judges each incoming row of an insert and the caller-read stored row of a by-id update in process, applies an unjudgeable row as the lock and says why; `canWriteObject` judges the payload on insert and the lock on update; explain's fls layer names each row-conditioned entry as such. - lint: `permissions[].fields[].when` is judged by the passes an RLS predicate takes, against the key's object. - hono: the fields merge reads the one core function. Clause-②: yes (widening) Metadata-protocol: added Ruling-ref: 6408558941 Claude-Session: https://claude.ai/code/session_01N3GzdgZRZ3z4wYfkWX1RJd Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 154 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b0c06d7c3b53aefa701b45c388951b0696b11f13 && git checkout b0c06d7c3b53aefa701b45c388951b0696b11f13
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0479cd6a0a6abb77b8180f7cf14e5eda778cce42 3d3775861b6d3c6dbf87402fbe71d64703127399 && git checkout -B drift-repro 0479cd6a0a6abb77b8180f7cf14e5eda778cce42 && git merge --no-ff 3d3775861b6d3c6dbf87402fbe71d64703127399
node scripts/docs-audit/affected-docs.mjs --json 0479cd6a0a6abb77b8180f7cf14e5eda778cce42
|
… row condition (#25866) Both pages stated that a field permission entry has exactly two keys; the Docs Drift Check on the PR named them. `field-level-security.mdx` gains a "Row-conditioned edit locks (`when`)" section and `protocol/objectql/ security.mdx` adds the key to the reference block with a note. No release-owned page is touched. Claude-Session: https://claude.ai/code/session_01N3GzdgZRZ3z4wYfkWX1RJd Co-authored-by: Claude <noreply@anthropic.com>
…hen` the RLS predicate rule judges (#25866) Claude-Session: https://claude.ai/code/session_01N3GzdgZRZ3z4wYfkWX1RJd Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #25866 (body and all six comments: ruling 6408558941 letter A, premise check 6409890004, Claim 6410022810, os-dev-report 6411406490), PR #25980 (body, 28-file list, net diff against main from merge-base 38050ce: +1132 / -96), and the 55 check-runs on the head: 46 success, 9 skipped (opt-in harnesses and matrix placeholders), 0 failure, 0 pending at the time of this record; all eight required contexts success. PR state while reviewing: open, not merged, approved by os-zhuang (review 5648858472 on 94d1fde, not dismissed), auto-merge armed by os-zhuang. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对REWORK (patch round, same thread) · domain:spec seat 2 · 2026-10-26T08:33Z · head The contract review on this head is PASS (comment 6411620876): the spec key, the in-process row judge, fail-closed locking, the merge and the semver all hold. The PR is incomplete against the ruling it closes, so it does not land as
The claim's file surface is amended to include the form. After the push, the same-head contract review is re-run on the new head. The maintainer's approval needs a fresh look after the push. |
…sion `when` (#25866) Patch round on PR #25980 (seat verdict 6411666422; contract review 6411620876 PASS). - PermissionMatrixEditor: each field row carries a When (edit) column. An authored condition shows as a chip, and the row opens the same CEL editor the RLS clauses use (CelPredicateField, clause check), linted the same way; a blocking finding holds the save as an RLS clause does. Read off clears the condition (the spec refuses `when` beside `readable: false`, so that shape is never authored); Read all / Write all keep it; Clear drops the entry; blank text removes the key. FieldPerm gains `when`. - i18n: en and zh rows for the column, the editor label, the affordances and the tip. - Tests: PermissionMatrixEditor.fieldWhen-25866 — offer, show and save; a stored condition on load; Read off clears; toggle and bulk keep, Clear drops; blank removes the key; a refused condition holds the save and a fixed one releases it. - ADR-0066 ⑧ note: the garbled last sentence rewritten. - Docs: field-level-security names the Studio control; the changeset adds @object-ui/app-shell (minor) and the Studio bullet. Clause-②: yes (widening) Metadata-protocol: added Ruling-ref: 6408558941 Claude-Session: https://claude.ai/code/session_01N3GzdgZRZ3z4wYfkWX1RJd Co-authored-by: Claude <noreply@anthropic.com>
…erged one of #25980 stays as it landed The patch round first widened `.changeset/25866-field-permission-row-condition.md`, which #25980 had already merged to main (squash c8cb510). A changeset that existed at the merge base is not this change's to edit (check-changeset-overwrite), so it is restored byte-for-byte and the Studio form is declared in `.changeset/25866-studio-field-permission-when.md` (@object-ui/app-shell patch; Clause-②: yes (widening); Metadata-protocol: none). Claude-Session: https://claude.ai/code/session_01N3GzdgZRZ3z4wYfkWX1RJd Co-authored-by: Claude <noreply@anthropic.com>
Requested by Steedos · project thread
Fixes #25866
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6408558941
What lands
FieldPermissionSchemagains an optionalwhen: a row condition in the same canonical CEL asrowLevelSecurity[].using, on the edit vote only (letter A of the ruling, comment 6408558941).packages/spec/src/security/permission.zod.ts).whenis a non-blank string;readable: falsebeside it is refused at parse with a message that says the condition reaches the edit vote only;condition/where/filterare refused with a pointer towhen. The describe states the whole contract (absent votes on every row; present votes as declared on matching rows and is absent elsewhere; stored row before an update, incoming row on an insert; unjudgeable rows are the lock; validated like an RLS predicate; row-blind readers report the lock). Generated artefacts regenerated (authorable-surface/security.json,content/docs/references/{api/protocol,security/permission}.mdx,liveness/state-counts/permission.md);dropped-refinements.baseline.jsongains thesecurity/FieldPermissionroot site and the sixfields.valueTypesites the build printed;liveness/permission.jsonclassifiesfields.whenlive with its readers.effective-object-permissions.ts).fieldPermissionVote(entry, verdict)is the ONE reading of an entry under a tri-state verdict (matched/unmatched/unjudged), andbuildEffectiveFieldPermissions(sets)is the row-blind most-permissive merge the hono endpoint now calls. Bits are read=== true: identical to the previous merges for a parsed entry (the schema materialises both booleans), and fail-closed where the old truthiness read was wider — a malformed stored entry carrying a non-boolean truthy value no longer grants (contract review 6411620876 ①.8).rls-compiler.ts). Thecurrent_userbuilder is extracted fromcompileFilterinto one privateuserContextOf(reserved-key refusal included), andcompileRowPredicate(expression, executionContext, fieldGuard?)compiles ONE predicate through the same compile, field-existence judge and comparand faces acheckclause takes — no OR-combination, no deny sentinel. ⛔ Not theql.findOnequery rewrite of theusinggate: the row is judged in process.permission-evaluator.ts).getFieldPermissions(object, sets, judge?)votes each entry throughfieldPermissionVote; without a judge a conditioned entry isunjudged(its lock).conditionedFieldEntries(object, sets)lists them for explain.security-plugin-object-inputs.ts→computeForbiddenFieldWrites, now async, with arowargument). Fast path when no set carries awhenfor the object. Otherwise each distinctwhenis compiled once (compileRowPredicatewith the object's field guard) and judged by the gate's ownstoredFormCheckJudge: each incoming row of an insert on its own values (the union is refused), the caller-read stored row of a by-id update (getCallerPreImage, fetched lazily and only when awhenexists). A null pre-image, a predicate update, a reader that holds no row, or a condition that yields no filter for this request applies the entry as its lock and WARNs why. Wired at the middleware's field step (2.5) and incanWriteObject(security-plugin-object-admission.ts: inserts judged on the payload, updates applied as the lock since the method holds no row).explain-engine.ts). Theflslayer appends a sentence naming each row-conditioned entry as such:amount (locked when status == 'approved', set "hr_user")— judged per row by the write gate, applied as the lock elsewhere.validate-rls-predicate-enforceability.ts). A newwhenclause:permissions[].fields["<object>.<field>"].whenis walked by the passesusing/checktake, against the key's object (the head before the first.), with the cost named: the entry cannot be judged and is applied as its lock on every row. Ids: the existingrls-predicate-*five.current-user-endpoints.ts). The inlinefieldsmerge of/auth/me/permissionsisbuildEffectiveFieldPermissions(resolved): row-blind, so a conditioned entry is reported as its lock and no client is told of an edit the gate refuses.permission-evaluator.tsalready pins ADR-0066, so no anchor file changes..changeset/25866-field-permission-row-condition.md:minoron spec, core, plugin-security, lint, plugin-hono-server.Readers of
getFieldPermissions— each one's conclusionsecurity-plugin-explain-access.ts(getFieldMask,getPartialMaskRules)flslayer. No change to the mask derivation.security-plugin-field-readers.ts(writable-field projection, :188/:212)security-plugin-object-inputs.tscomputeQueryGuardFieldPerms(:460/:466) andmaskOperationResultpackages/lint/src/validate-security-posture.ts:755 / :814whenchanges neither. The predicate itself is judged invalidate-rls-predicate-enforceability.ts(see deviations). Unchanged code.packages/core/src/security/effective-object-permissions.ts~:546objectsmerge is untouched; thefieldsmerge now lives beside it (buildEffectiveFieldPermissions) and reads the lock.plugin-hono-server/src/current-user-endpoints.ts~:900Landing-point deviations from the Claim (6410022810)
validate-rls-predicate-enforceability.ts, notvalidate-security-posture.ts. The ruling says "validated like an RLS predicate (field names exist, no cross-object)". That rule already owns the compiler-backed passes (isSupportedRlsExpression, the reference pass, the comparand faces) and the fiverls-predicate-*ids; a second spelling invalidate-security-posture.tswould be the drift the rule exists to avoid.validate-security-posture.tsis unchanged.plugin-hono-server/src/current-user-endpoints.tsis touched (outside the Claim's parenthetical list, inside its "other readers" obligation): it carried an inline copy of the field merge, which would otherwise have reported a conditioned entry as editable.json-schema: the build also rewritesauthorable-surface/security.json, two reference.mdxpages, the dropped-refinements ledger and the liveness ledger + state count. All in the Claim's "+ generated spec artefacts".content/docs/permissions/field-level-security.mdxandcontent/docs/protocol/objectql/security.mdxgain awhenparagraph (both stated that a field permission entry has exactly two keys), and the validation matrix row incontent/docs/deployment/validating-metadata.mdxnow names the field permissionwhenamong what the RLS predicate rule judges. The Docs Drift Check on this PR named all three; leaving the sentences false is the drift the documentation guardrail exists to stop. Hand-written trees are editable per AGENTS.md; no release-owned page is touched.Not measured here
when— patch round of #25980 (#25866) #26010 (this PR merged before that round landed). The first revision of this bullet said the Studio lives in../objectui, unreadable from this seat — wrong: the Console source is in this tree underui/, and feat(ui): the Studio permission-set form offers the field permissionwhen— patch round of #25980 (#25866) #26010 changesui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx.approvedonward).维护者速读(草稿)
when(与 RLSusing同一 CEL),只作用于 editable;readable: false与when同时出现在解析期拒绝。compileRowPredicate+storedFormCheckJudge),不走查询改写;无法判定(谓词更新、读不到的行、本请求无法解析的条件)按锁处理并 WARN。/auth/me/permissions、字段投影、os explain)一律按锁上报,保证不比写门更宽。validateRlsPredicateEnforceability的全部通道判定permissions[].fields[].when。Acceptance notes
permission.test.ts— conditioned lock/grant accepted,readable:false+whenrefused atwhenwith the stated message, blank/null refused, aliases pointed atwhen, rides onPermissionSetSchema.fieldPermissionVoteunder the three verdicts;buildEffectiveFieldPermissionsrow-blind lock, union without deny, fresh entries.security-plugin.test.ts): insert draft admitted / approved refused / bulk union; by-id update judged on the stored row (approved pre-image refused even when the payload moves to draft; draft pre-image admitted even when the payload approves) with ONE caller-context by-id read; predicate update → lock + WARN; null pre-image → 404 upstream at 2.7 and the gate's own derivation → lock + WARN;current_user.idresolves against the caller; unresolvablecurrent_user.nope→ lock + WARN;canWriteObjectinsert-on-payload / update-as-lock. Evaluator: matched / unmatched / unjudged votes, union with a sibling grant,conditionedFieldEntries. Explain: theflssentence.whensite, path/where/message, silent on a valid condition, key-object targeting, blank skipped.Verification
See the
os-dev-reportcomment on #25866 for the gate run record (exit codes captured before any pipe, reconciled withdispatch-gates --ran) and the per-package typecheck/test results.Generated by Claude Code
Patch round (seat verdict 6411666422) — landed as PR #26010
This PR merged at 08:51Z, before the seat's patch round on it was pushed. The round is PR #26010 on the same branch, rebuilt from
main: the Studio form (offer and showwhenon the edit vote; the Read-off cascade clears it and the bulk shortcuts keep it; tests), the rewritten last sentence of the ADR-0066 ⑧ note, and the two corrections to this body (the Studio bullet above, and the "byte-identical" claim under Core).🤖 Generated with Claude Code
https://claude.ai/code/session_01N3GzdgZRZ3z4wYfkWX1RJd