Repository navigation
fix(plugin-security): an assignment write the administrator cannot address answers not-found, as a read does; a refusal names only values the caller can read - #30165
Conversation
…dress answers not-found, as a read does A by-id update or delete of an assignment row that the organization administrator or the delegate does not read answers the read door's not-found for that object and id; a refusal names only values the caller can read. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…fusal wording for assignment writes Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…t-found answer this change gives Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…umn its field rules hide Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…a string Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 14 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 — 17 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 c1b0c7fdefb12030d08200763af13fd60714577e && git checkout c1b0c7fdefb12030d08200763af13fd60714577e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eca7cd2008c7aa5174b451428b8b73b29b237299 da7d298fd20cf6053cd62936c1e2baa829452bac && git checkout -B drift-repro eca7cd2008c7aa5174b451428b8b73b29b237299 && git merge --no-ff da7d298fd20cf6053cd62936c1e2baa829452bac
node scripts/docs-audit/affected-docs.mjs --json eca7cd2008c7aa5174b451428b8b73b29b237299
|
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head f29ee6a · 2026-11-04T18:25Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Reviewed against rulings B and sub-item B (maintainer, 2026-11-04, batch 4 item 2) and the governing ruling of record 5985885287 on #21771; prior state is #29932 and #29950 (merged). Read-only: the diff from the merge base, the PR body, the check-runs on the head, and the cited code at the head. Nothing built or run. ① Derived judgmentsRule 1 — a by-id assignment write the administrator cannot address answers the read door's not-found. Holds, and holds as one producer.
Rule 2 — a refusal names a stored value only when the caller can read it. Holds for the delegated branch; the organization branch is addressed under ③ (2).
§6 ( ② Semver level
③ Boundary flags
④ Design judgmentTried to break the rule; no must-fix found.
Findings: no must-fix. Should-fix: ③ (2) the organization branch's verdict on the stored row; ② the Unchanged clause of Implemented-by: VERDICT: PASS |
…r form; the pending note's edit-in-place clause names the readable row Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Delta review by the same reviewer, on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Findings: no must-fix; no new should-fix. Two of the three earlier should-fix items are applied; the third is deferred to a separate order by the seat. Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Red by design · head Gate: Cause: this PR edits two pending changesets it did not create, Why the red does not block anything else: Still owed: the maintainer's written confirmation on this PR. The seat will not land the PR until it is posted. |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Changeset correction confirmed by the maintainer, 2026-11-05T00:47Z. The confirmation was given in session This is the written confirmation that |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
Clause-②: no
Metadata-protocol: none
维护者速读
sys_user_position、sys_user_permission_set)时,如果这一行自己读不到,或这个 id 根本没有对应的行,现在都回答读取门的404 RECORD_NOT_FOUND,和读取这个 id、以及其他任何对象按 id 写入时的回答完全一样。委派管理员被拒时,提示和 details 里只出现调用者自己读得到、或自己写进去的值。security-plugin-authz-middleware.ts里readDoorAnswerForGateRefusal新增的 not-found 分支;delegated-admin-gate.ts里的UnreadableAddressedRowRefusal、delegatedTargets和mayName;组织管理员分支organizationAdminFailure对 update 先问一次读取。pnpm --filter @objectstack/plugin-security exec vitest run src/org-admin-assignment-boundary.posture.test.ts,预期 74/74 通过。check-empty-changeset在你确认之前按设计保持红。What changes
recordNotFoundErrorfor that object and id: the read door's404 RECORD_NOT_FOUND, which step 2.7 already answers for every other object. This covers the organization administrator's branch and the delegated branch (a delegate holding anadminScope, and an organization administrator who holds one too). A row the caller cannot read and an id naming no row get the same answer, and it is the same answer aGETof that id gets.UnreadableAddressedRowRefusal(aPermissionDeniedError). OnlyreadDoorAnswerForGateRefusalturns it into the not-found, and only for the by-id write the caller addressed (addressedByIdWriteId, with step 2.7's exclusions). A nested write, a referential clear and the write-admission preview keep the gate's403, which names no row and no id.403).mayName). Otherwise the sentence is the same without the value, for example "the assignment's position distributes a permission set which is not in the scope's allowlist", anddetailsleaves the value out. The verdict does not change: it is still taken on the stored row.readAsCaller, the operation's memoised pre-image read). A column a field rule hides is absent from that read, and a masked column differs from the stored value, so neither is named..changeset/org-admin-removal-caller-read.mdand.changeset/delegated-admin-unreadable-row.mdsaid the answer is403 PERMISSION_DENIED. This change makes that false, so each now names the not-found.Tests
org-admin-assignment-boundary.posture.test.ts(real ObjectQL engine and SQL driver, realSecurityPlugin):groupandisolated, for both tables, both verbs, and a row the caller cannot read as well as an id naming no row:recordNotFoundErrorfor that id;DELEGATION_SCOPE_VIOLATION, and its value appears nowhere in the answer. The delegate who reads the column gets the same verdict, with the value named. A value the caller wrote is named. An organization administrator whose field rules hide the column is refused without the value.position, notreadable: false): the delegate reads the masked value, and neither that value nor the stored one appears anywhere in the serialized refusal. The verdict still comes from the stored row: refused for a stored position off the allowlist, admitted for one inside the scope.single, rows the caller reads keep their guidance and an id naming no row answers the not-found.isolated):org-admin-assignment-removal-unreadable-rowanddelegated-admin-assignment-write-unreadable-rownow pinPATCHandDELETE, of the row and of an id naming none, to equal exactly what aGETof that id answers (404 RECORD_NOT_FOUND, status and body). Each has a measured red leg, below.Acceptance notes
.changeset/org-admin-removal-caller-read.mdand of.changeset/delegated-admin-unreadable-row.md. Neither has shipped (the plugin'sCHANGELOG.mdholds neither, and npm has18.3.0). Both said a row the caller cannot read answers403 PERMISSION_DENIED, which this change makes false. Each now says the read door's404 RECORD_NOT_FOUND.check-empty-changesetnames both files and stays red by design until a maintainer confirms. Restoring them from the base would republish the false sentence.ui/treats a403and a404from these writes differently. They list only rows the caller reads, so the new answer reaches them only when a row was removed in between. That answer now reads "Record … not found in …" where it used to read as an access refusal.Verification
Readings at
da7d298fd2, this body's head: the plugin typecheck, the posture rig, ablation A3 and the derived gates. Readings atf29ee6a49d: the whole suite and the dogfood legs. Since then only the posture rig's test file and one pending changeset's wording have changed. Readings atddb6927721: ablations A1 and A2. The one commit betweenddb6927721andf29ee6a49dtypes a test's assertion label as a string. CI's families are this PR's own check-runs;test:repoand the whole-tree lint are CI's.plugin-security: typecheck, the whole suite, the posture rig
One-shot ablations of the posture rig (its subject resolves from this package's own source)
HTTP dogfood: the two re-pinned files, with a measured red leg
Derived gate families (dispatch-gates --commands: 64)
Full dogfood battery: not owed (no
!in the title, no narrowing arm).