Repository navigation
fix(plugin-security): a View All / Modify All set reads every field of its object, so an app field mask no longer hides fields from admins - #29387
Conversation
…29106) A set holding viewAllRecords/modifyAllRecords on an object votes on every field of it (ruling of record 6573563853). Pins the evaluator merge, the row-blind /auth/me/permissions merge, one super-user predicate, and the HTTP doors on a real boot. Claude-Session: https://claude.ai/code/session_01ADHkmSgjH5Vi4jT3KVJpvt Co-authored-by: Claude <noreply@anthropic.com>
…d of its object (#29106) ADR-0066 D2 ⑧ as amended by the ruling of record 6573563853 (letter A): in the field-permission merge a set holding viewAllRecords on an object votes readable on every field of it, and one holding modifyAllRecords votes readable and editable. Who holds the bypass is read by one function, holdsSuperuserBypass (@objectstack/core), which superuserBypassSets, the evaluator's getFieldPermissions and the row-blind /auth/me/permissions merge (buildEffectiveFieldPermissions) all read. Union without deny is otherwise unchanged. Two docs pages updated to match. Claude-Session: https://claude.ai/code/session_01ADHkmSgjH5Vi4jT3KVJpvt Co-authored-by: Claude <noreply@anthropic.com>
… by a row policy, not View All (#29106) A set holding viewAllRecords on sys_user now votes readable on every sys_user field (ruling of record 6573563853), so the fixture's own email mask could not hold. The fixture's intent (every row, email withheld) is kept through a select policy instead. Claude-Session: https://claude.ai/code/session_01ADHkmSgjH5Vi4jT3KVJpvt Co-authored-by: Claude <noreply@anthropic.com>
…29106) So an ablation of the super-user vote reds the admin half while the member control is read green on its own. Claude-Session: https://claude.ai/code/session_01ADHkmSgjH5Vi4jT3KVJpvt Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check8 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 46 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 586b287fc8349804d392c5509fa17064ba424aa6 && git checkout 586b287fc8349804d392c5509fa17064ba424aa6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7f277a86f670a956dcdf677937c1894706992bee 252a02f4aeec75996fa0ff5443b659b68a1fe1ad && git checkout -B drift-repro 7f277a86f670a956dcdf677937c1894706992bee && git merge --no-ff 252a02f4aeec75996fa0ff5443b659b68a1fe1ad
node scripts/docs-audit/affected-docs.mjs --json 7f277a86f670a956dcdf677937c1894706992bee |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Contract reviewServed-tier: Inputs: card #29106 (body, every comment: triage 6572001650, analysis 6572555344, director 6573082985, ruling 6573563853 letter A, claim 6576911116, precondition grep 6576959092, os-dev-report 6578856250, claim amendment 6578888075), PR #29387 body and file list, net diff against merge-base ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentNo
Findings:
Implemented-by: VERDICT: PASS |
…rite depth, not Modify All (#29106) deal_manager held modifyAllRecords beside an editable:false lock on restricted_note. A set holding the super-user bypass now votes editable on every field of its object (ruling of record 6573563853), so the lock could not hold and the refused-node case went red. The manager now reaches every deal through readScope/writeScope 'org'; every assertion is unchanged and the GUARD case still holds. Claude-Session: https://claude.ai/code/session_01ADHkmSgjH5Vi4jT3KVJpvt Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Contract reviewServed-tier: This record supersedes 6579089871 (PASS on Inputs: card #29106 (body and every comment, the ruling 6573563853 letter A and the precondition grep 6576959092 included), PR #29387 body (as amended for FAIL round 1), its file list, the net diff ① Derived judgments
② Semver levelUnchanged from 6579089871: ③ Boundary flags
④ Design judgmentNo
Findings:
Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt 席位核对ACCEPT · The maintainer's ruling for this card: 6573563853, letter A, 「同意」 2026-11-02T09:22Z.
Generated by Claude Code |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Landing record · PR #29387
Generated by Claude Code |
Requested by Steve · project thread
Part of #29106. This is the runtime half. The ADR-0066 D2 ⑧ note is the card's second PR, governed Tier H, and it opens from this branch once this one merges.
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6573563853
维护者速读
viewAllRecords)的权限集,在字段合并时对该对象每个字段投「可读」;持「修改全部」(modifyAllRecords)的投「可读+可编辑」。判断谁是超级用户只用一个函数,行级和字段级共用。readable: false(已在卡上贴查证结果)。回滚 = revert 本 PR。packages/core/src/security/effective-object-permissions.ts(holdsSuperuserBypass、superuserFieldVote)→packages/plugins/plugin-security/src/permission-evaluator.ts(getFieldPermissions、superuserBypassSets)→packages/qa/dogfood/test/fls-superuser-vote.dogfood.test.ts。pnpm --filter @objectstack/dogfood exec vitest run test/fls-superuser-vote.dogfood.test.ts,预期 11 条全绿:管理员看到全部字段并能按code排序,普通成员仍被遮、排序 403。What changes
getFieldPermissions(per object) andbuildEffectiveFieldPermissions(the row-blind/auth/me/permissionsmerge) applysuperuserFieldVoteafter the declared-entry votes. A set holdingviewAllRecordson the object votesreadable: trueon every field of it. A set holdingmodifyAllRecordsvotesreadable: trueandeditable: true. View All never grants an edit. The vote touches only fields the merge already holds an entry for, because an absent field is already unrestricted.holdsSuperuserBypass(set, object, bit)is new in@objectstack/core.PermissionEvaluator.superuserBypassSetsnow reads each set through it, and so doessuperuserFieldVote. The row bypass and the field vote therefore name the same sets: the set's own entry for the object if it has one, otherwise its'*'wildcard. The private posture never changed that answer, since it withholds only a wildcard that carries neither bit. The new pinthe private posture never changes the super-user answerholds it./auth/me/permissionsandPOST /security/explainall read one of those two merges.requiredPermissionscapability gate still applies after the merge.content/docs/permissions/permissions-matrix.mdx§3: the Sales Manager column, whose set holds View All oncontactand Modify All onopportunity, plus one sentence on why.content/docs/permissions/field-level-security.mdx: the union paragraph now names View All / Modify All..changeset/29106-superuser-field-vote.md:patchfor@objectstack/plugin-securityand@objectstack/core.Tests
packages/plugins/plugin-security/src/fls-superuser-vote.test.ts(new, 25 cases). It covers the card's repro against the shippedadmin_full_access, order independence, the member control, the plain-read control, view-only-never-edits, modify reads and edits, a wildcard shadowed by an explicit entry, a vote held on another object, and undeclared fields staying absent. It also pins one predicate (field votes equal thesuperuserBypassSetsanswers, and the private posture never changes them) and parity between the row-blind and per-object merges.packages/core/src/security/effective-object-permissions.test.ts: two cases, one for the row-blind vote and one forholdsSuperuserBypass.packages/qa/dogfood/test/fls-superuser-vote.dogfood.test.ts(new, 11 cases, real showcase boot). The card's repro onsys_business_unit: an admin holdingadmin_full_accessplus an app set withreadable: falseoncode,kind,manager_user_idandactive. Each door is checked as an admin case and a member control case:/auth/me/permissions, list, record read, sort and filter, and explain.packages/plugins/plugin-security/src/explain-principal-person.test.ts. Itsqa_pp_directory_maskedfixture heldviewAllRecordsonsys_userandreadable: falseonsys_user.email. Under the ruling that set reads its own masked field, so 4 cases went red. The fixture keeps its intent (every user row, email withheld) by reaching the rows through aselectrow policy instead of View All. All 16 cases are green, and the "armed" cases measure the read door itself.packages/services/service-automation/src/resume-decider-identity.integration.test.ts. Itsdeal_managerfixture heldviewAllRecords+modifyAllRecordsondealbeside'deal.restricted_note': { editable: false }. Under the ruling the lock is out-voted by the set's own Modify All, so the decider's write landed anda post-decision node the decider may not perform is refused loudlywent red in CI (Test Core 8/8). The manager now reaches every deal throughreadScope: 'org'/writeScope: 'org'instead of the super-user bits. Every assertion is unchanged. The GUARD case still shows the manager's write rests on the manager's own grants. 21/21 green.Acceptance notes
readable: false/editable: falseentry on the same object, across the default sets, the showcase, the hotcrm fixture, skills and docs code blocks. The one docs near-hit (the permissions matrix) is fixed here at seat 2's request.viewAllRecords/modifyAllRecordsbesidereadable: false/editable: falseon the same object. Shipped and app metadata: 0 hits. Tests: the two forced edits above, pluspackages/qa/dogfood/test/admin-ledger-decision-metadata.dogfood.test.ts. That file'sald_withhold_set/ald_withhold_only_setpair View All onsys_userwithemailwithheld, so thewithheldclass no longer applies to those readers. Its assertions are about which ledger rows are served (none about the subject, through every door), not about the field, and it stays green. Noted here, not edited./auth/me/permissionsmerge lives in@objectstack/core(buildEffectiveFieldPermissions), not incurrent-user-endpoints.ts, so the diff touchespackages/coreand leavesplugin-hono-serverunchanged. Seat 2 accepted this file-surface change.Verification
Head
252a02f4(FAIL round 1: one test-fixture commit on top of718ba1b2; origin/main47718699merged in). The gates and the full dogfood battery below ran on718ba1b2; the round-1 commit changes one service-automation test file only.Red first on main (5780fa4 + tests only)
fls-superuser-vote.test.ts: 7 failed / 18 passed. Every admin vote case failed (expected { readable: false, editable: false } to deeply equal { readable: true, editable: true }). Controls and parity were green.fls-superuser-vote.dogfood.test.ts(first layout): 6 failed / 2 passed. The admin sort was refused with the card's own message:403 … references field(s) not readable by the caller: code. Explain reported4 field(s) masked from responses: [code, kind, manager_user_id, active]for the admin. The member's/auth/me/permissionscontrol was green.Green after the fix
fls-superuser-vote.test.ts25/25 · coreeffective-object-permissions.test.ts43/43 · dogfoodfls-superuser-vote.dogfood.test.ts11/11.tsc+check:test-typecheck) passed for core, plugin-security and plugin-hono-server after a DTS build.eslint --no-inline-configoverpackages/core/src,packages/plugins/plugin-security/srcand the new dogfood file: exit 0.Ablation of the new vote
superuserFieldVotewas made to returnnullfirst, throughscripts/ablation-replace.mjs. MarkerABLATION_29106_NO_SUPERUSER_VOTE;ablation-dist-preflightshowed it inpackages/core/dist/index.jsandindex.cjs./auth/me/permissions, list, record read, sort+filter, explain. Green: premise and all 5 member controls.09e5a75c, core rebuilt,ablation-dist-preflight --absentexit 0,git status --porcelainclean.Gates and full dogfood (718ba1b)
node scripts/pm/dispatch-gates.mjs --commandson718ba1b2: 94 derived families, all run. 93 exit 0 on the first pass.check-plugin-teardown-shape.mjs --self-testexited 3 because its positive control commit was missing in the shallow clone; aftergit fetch origin 621a4876…it exits 0 (48 cases).--ranreconciliation: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN.Full dogfood battery on
718ba1b2after a fullturbo run build --filter=./packages/** --filter=./examples/**(10m41s under the lock):pnpm --filter @objectstack/dogfood test→ VERDICT command-exit 0 · Test Files 394 passed / 3 skipped (397) · Tests 3254 passed / 28 skipped (3282) · no failures.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmSgjH5Vi4jT3KVJpvt