Repository navigation
fix(spec)!: an object permission that edits or deletes a record also reads it - #30180
Conversation
… entry; a refused read answers every addressed by-id write Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
…ecord also reads it — stored rows, cross-set read, and the read-refused caller's by-id writes Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
…elete descriptions Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
…lete and clears them with Read; dogfood pins at the HTTP door Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
…as read ∧ grant Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
…data door Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
…so reads it, at the HTTP doors; stored edit-without-read personas Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
…ad door refuses Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 173 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 f7ed8e3b38f0a9e85ca355d145fd132402e0e977 && git checkout f7ed8e3b38f0a9e85ca355d145fd132402e0e977
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a35fc6656636772766917e79c4d4692d0b7fe216 58698caa351d546295248ade5a30c1040e140262 && git checkout -B drift-repro a35fc6656636772766917e79c4d4692d0b7fe216 && git merge --no-ff 58698caa351d546295248ade5a30c1040e140262
node scripts/docs-audit/affected-docs.mjs --json a35fc6656636772766917e79c4d4692d0b7fe216
|
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head d7bf39c · 2026-11-04T22:17Z |
…te once nothing reads; the upgrade report's edit cell reads through the fold Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Delta review by the same reviewer, on top of the FAIL record at ① Derived judgments
② Semver levelUnchanged from the first record: The dev's open question — should the changeset's ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
…emains Claude-Session: https://claude.ai/code/session_01QSeKYkouVh79J2UzzrhWsr Co-authored-by: Claude <noreply@anthropic.com>
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head Regen-provenance:
|
Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting
allowEdit: trueorallowDelete: truewithout read (allowRead,viewAllRecordsormodifyAllRecords), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened.Metadata-protocol: narrowed
Skills-docs: docs moved —
content/docs/permissions/permission-sets.mdxstates the rule under the object permission bits;skills/**teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves.维护者速读
packages/spec/src/security/permission.zod.ts(规则与运行时折叠),packages/plugins/plugin-security/src/permission-evaluator.ts(按调用者合并后的条目判定),ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx(勾选联动)。pnpm --filter @objectstack/spec exec vitest run src/security/permission.test.ts -t "also reads it",预期 11 条全绿。What changes
The rule: an object permission that edits or deletes a record also reads it.
ObjectPermissionSchema(and soPermissionSetSchema,definePermissionSet,defineStack,os validate/os buildand the metadata save door) refuses an entry grantingallowEditorallowDeletewith none ofallowRead,viewAllRecords,modifyAllRecords: onecustomissue per bit at the bit's own path, naming both fixes.objectPermissionGrantsanswers edit and delete asread ∧ grant, the shapeallowExportalready had. The response-sideEffectiveObjectPermissionSchemastays tolerant. TheallowEdit/allowDeletedescriptions state the rule (reference docs regenerated). ADR-0087 D3 semantic entrypermission-write-without-read-refusedwith FROM → TO; no D2 conversion, because either rewrite would change an access decision.plugin-security).PermissionEvaluator.checkObjectPermissionasks the fold of the caller's MERGED entry (the most-permissive per-object merge/me/permissionshands the client), so the read half may come from another set, exactly as export's does, and the door and the client map give one answer. Object admission (canReadObject/canWriteObject),security/explainand the middleware gate all read through it. A permission set stored before this release still loads; its unread edit or delete grant grants nothing.ui/(@object-ui/app-shell). The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry (clearing Delete beside Modify All would author the pair the spec refuses).content/docs/permissions/permission-sets.mdxstates the rule.Not changed:
allowCreateandallowTransferare outside this rule.Tests
permission.test.ts› "an object permission that edits or deletes a record also reads it" (refusal per bit and path, wildcard, stack schema, every read bit accepted, wire tolerance, the fold).permission-evaluator.test.ts(exhaustive fold matrix against an independent reference, stored rows, cross-set read);by-id-write-unreadable-not-found.test.ts› "a caller the read door refuses gets that refusal from every by-id write it addresses, a row or none" (read gated on a capability; with and without row-level policies; capability-holder control).controlled-by-parent-*,explain-asker-gate,predicate-write-unreadable-not-matched,org-admin-assignment-boundary.posture,get-effective-object-permissions,data-conflict-caller-visibility,share-links-enforcement-context; lint's access-matrix reference fold.ui/:PermissionMatrixEditor.writeImpliesRead.test.tsx(both directions, saved payload parses).object-permission-write-implies-read.dogfood.test.ts(save door 422 at the bit's path; a stored set without read: explain denies,PATCH/DELETEone refusal for a row and an id naming none, row untouched; control with read).upsert-by-id-caller-read.dogfood.test.tsseeds its edit-without-read personas as stored rows.Acceptance notes
packages/**,examples/**,skills/**,content/docs/**,ui/**found 48 such entries, all in tests or in code guarded bymodifyAllRecords; the showcase, CRM and multi-package example apps build under the new rule. Every test fixture that authored the shape was triaged: built raw as a stored pre-upgrade row (it now pins what such a row grants), never re-spelled to hide it.requiredPermissionsmap that gatesreadand notupdate/delete(ADR-0066 ⑤). The rule above covers it at the by-id write door; pinned with and without row-level policies, plus a capability-holder control.allowCreateandallowTransferwithout read.access-upgrade-report.tsreads a customer set's edit grant throughgrantsEditBit(the declared bit and the read the fold requires), so a stored entry without read, which edits nothing, is not reported; it does not fold in View All / Modify All holders, whose meeting with a package's field denies is a separate question.ui/type-check NOT MEASURED:@object-ui/app-shelltype-checkreports 1096 TS2307 (the@object-ui/*workspace packages have no built declarations in this container) and the TS7006 cascade they cause, none on an edited line; CI'sType Check UImeasures it.Verification
Readings on head
403f77c0unless named.d7bf39c3changed one code comment and the changeset wording;f4a273bbapplied the review's should-fixes (editor cascade for every reading bit,grantsEditBit) with 113 derived families run, 0 NOT MEASURED, plugin-security 242 files / 5283 tests, the ui targeted set 75 files / 607 tests;58698caais prose only (changeset and docs wording), 54 derived families run, 0 NOT MEASURED.Ablation of the runtime conjunction (one shot, restored)
objectPermissionGrants'allowEditcell mutated topermission.allowEdit === true || modifyAllthroughscripts/ablation-replace.mjs, spec rebuilt,ablation-dist-preflightfound the marker in 2 built files. Red: specpermission.test.ts1 failed / 121 ("the fold grants neither verb to such an entry: read ∧ grant"); plugin-securitypermission-evaluator.test.ts3 failed / 38 (the exhaustive matrix, "update: … a grant without read grants nothing", "update: the read half may arrive from a different set"); dogfoodobject-permission-write-implies-read1 failed / 8 ("explain denies both"). The by-idPATCHpin stayed green under the mutation, because the read-refusal step refuses the write on its own. Restore: blob equals HEAD (27bdf93c5901),git diff HEADempty, spec rebuilt, marker absent from all 592 built files, tree clean.Suites
@objectstack/specpnpm test: 812 / 812 files, 24413 tests.@objectstack/plugin-securityfull: 242 / 242 files, 5282 tests.@objectstack/runtimefull: 543 / 543 files, 9826 tests.@objectstack/verifyfull: 48 / 48 files.@objectstack/core139 / 139,@objectstack/formula64 / 64,@objectstack/lint192 / 193 before the reference-fold fix, the one red file (build-access-matrix.test.ts) 8 / 8 after it,@objectstack/plugin-hono-server38 / 38.ui/: the permission editor, permission slice and advanced facet suites,packages/permissions/and everyui/census file: 82 / 82 files, 703 tests;PermissionMatrixEditor.writeImpliesRead9 / 9 atf4a273bb.Dogfood battery (full, once)
pnpm --filter @objectstack/dogfood testunder the verify lock: 417 passed / 1 failed / 3 skipped files, 3469 tests passed / 1 failed. The one failure is a 5000 ms timeout inpackage-commit-revert-path-package("control: a writable package's edit commit restores its own row in place"), taken while the gate sweep shared the box; the file alone passes 12 / 12.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 113 families, each run with its exit code captured before any pipe. Three first refused with exit 3 (check-plugin-teardown-shape --self-teston the shallow clone,check:skill-examplesandcheck:dual-build-cjs-loadson unbuilt packages); aftergit fetch --unshallowand building those packages, all three exit 0.--ran: "113 derived famil(ies) accounted for — 113 run, 0 NOT-MEASURED".spec check:generated: all 14 artifacts current after mergingmain.