Repository navigation
fix(plugin-security): an organization administrator's assignment removal is judged on the stored row, and its refusal names only values the caller can read - #30248
Conversation
…val is judged on the stored row, and its refusal names only values the caller can read The organization administrator's branch of the ADR-0090 D12 gate now takes a removal's verdict from the stored row once the caller's own read returns the row, as the delegated branch does (ruling of record 5985885287, sub-item B), and words its refusal through mayName. A row the caller cannot read, and an id naming no row, keep the read door's not-found. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…ged on the stored row, and its refusal names only values the caller can read The posture rig pins it on a real engine under 'group' and 'isolated' for a position or permission set hidden by a field rule or partially masked, the row's organization (the gate's own verdict), the read door's not-found for these callers, and a 'single' control. The field-rule test's organization block is re-pinned to the stored-row verdict. The gate's unit file pins the same rule, and that no stored read is made for a row the caller cannot read. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…is judged on the stored row Adds the plugin-security patch changeset, and corrects one sentence of the pending note .changeset/org-admin-removal-caller-read.md that this change makes false: the administrator's own read now answers whether there is a row to remove, not the verdict on it. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…ns removals only An edit in place is no part of the organization administrator's right, and where its refusal falls is not this describe's subject, so its control asks the removal alone. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…enrolled organization administrator with no field rules Keeps the caller's context type mutable, as the engine's context expects. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…t state the rule only Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…e columns it holds for end to end Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
# Conflicts: # packages/plugins/plugin-security/src/delegated-admin-gate.ts
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 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 3fbc14233b1bda266ab6bfd81aa844700ea4abb8 && git checkout 3fbc14233b1bda266ab6bfd81aa844700ea4abb8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 771b2db20ac08d78b7fec5c2bd1ee30de9aa26dc 0b0d0e216f41f563de1d94a86e9e985d7f673c3f && git checkout -B drift-repro 771b2db20ac08d78b7fec5c2bd1ee30de9aa26dc && git merge --no-ff 0b0d0e216f41f563de1d94a86e9e985d7f673c3f
node scripts/docs-audit/affected-docs.mjs --json 771b2db20ac08d78b7fec5c2bd1ee30de9aa26dc
|
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Review order: an at-tier review on a security hardening PR (the ruling's own landing condition). Read-only: the diff against origin/main, the PR body, the cited code at this head and the check-runs. Nothing built, run or re-run. Mechanism, where a judgment needed one, is in the reviewer's local security-note and nowhere in this record (security-findings.md section 6). ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
Disclosure rule (security-findings.md section 6) on the PR title, body, commit messages, both changesets, test names and code comments: each states the hardening rule (what the verdict is taken on, what a refusal names, what answers not-found); none describes a probe or how the pre-fix door could be made to misbehave. The ablation lines name the pins they red, not a recipe. No breach. ② Semver level
③ Boundary flags
④ Design judgmentFor
Findings: no must-fix. Should-fix: ② the second bullet of Implemented-by: VERDICT: PASS |
…ct cites its governing ruling record, and the corrected pending note is true of the tree - Every stored-row-verdict citation this branch added now names ruling of record 6649064874 (the ADR-0090 D12 assignment gate judges on the stored row, and its refusal names only what the caller can read): the gate's docblocks and comment, and the two new describe names. The citation of 5985885287 for the read door's not-found answer is kept. - .changeset/org-admin-removal-caller-read.md (the declared deliberate correction): a row the administrator reads is judged on the stored row and its refusal names a value only when the administrator's own read returns it as stored; the not-found bullet is scoped to a removal. Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…ion in the package names ruling of record 6649064874 Comments and one describe name only. The citations of the stored-row verdict and of the rule that a refusal names only what the caller can read now name ruling of record 6649064874: the columnsReadAsStored and mayName docblocks, the two delegated-walk comments, the delegatedTargets docblock (whose guidance sentence now says what mayName does), and the posture rig's field-rule describe. The citations of 5985885287 for the read door's not-found answer are kept. 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: the two findings-only commits since the PASS record on ① Derived judgments
§6 ( ② Semver level
③ Boundary flags
④ Design judgmentNo Findings: no must-fix; both earlier should-fixes closed; no new should-fix. Owed before a Tier S landing: the maintainer's written confirmation of this PR's changeset correction, the by-design-red comment on this PR for Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Changeset correction confirmed by the maintainer, 2026-11-05T04:23Z. 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)时,只要它自己读得到这一行,委派管理闸门(ADR-0090 D12)的组织分支就按存储行判定,和委派分支一样;拒绝提示只写出调用者自己读得到的值。position/permission_set上时,组织边界内的删除现在放行(18.2.0 即按存储行判定);边界外照旧拒绝;读不到的行与不存在的 id 仍回答 404;修改、插入、租户级管理员、委派分支都不变。回滚即撤销本 PR。packages/plugins/plugin-security/src/delegated-admin-gate.ts的organizationTargets(先以调用者读取判断有没有这一行,读到后取存储行)与organizationAdminFailure里三处mayName措辞分支。pnpm --filter @objectstack/plugin-security exec vitest run src/org-admin-assignment-boundary.posture.test.ts,预期 85/85 通过。.changeset/org-admin-removal-caller-read.md一句话的改写(见 Acceptance notes 第一条);在你确认之前Check Changeset按设计保持红。What changes
organization_admin/organization_admin_no_bypass) removes an assignment row by id. Whether there is a row is still asked of the caller's own read (callerRead, the read door's answer to this caller). Once that read returns the row,organizationTargetstakes the stored row (storedRow) and the organization, position and permission-set checks run on it, as the delegated branch'sdelegatedTargetsdoes. This follows ruling of record 6649064874 ("the ADR-0090 D12 assignment gate judges on the stored row, and its refusal names only what the caller can read"), which covers both branches. No second reader: the branch reusescallerRead,storedRow,columnsReadAsStoredandmayName, and its target type is nowDelegatedTarget. No stored read is made for a row the caller's read does not return.mayName). Otherwise the sentence is "the row does not belong to the organization you administer", "the assignment's position is not inside the organization boundary" or "the grant's permission set is not inside the organization boundary", anddetailsleaves the value out. A column the caller does read is still named, so the guidance stays specific where it can be.404 RECORD_NOT_FOUND. A read refusal or a store fault propagates as raised. Inserts, tenant-level administrators and the delegated branch are as before. The update path is untouched: an edit in place is refused before any row is looked up, as fix(plugin-security): an organization administrator's update by id is refused before the row is looked up, whatever the id names #30200 set it, and nothing here pins it.Clause-②: nobecause it is a runtime permission behaviour change, whichexecution-duties.mdplaces outside Clause ② (its negative boundary: runtime permission and security behaviour is the human-floor security/permission class), and no published contract surface moves: no spec schema, no export (the package's public API is unchanged), no error code. For history: 18.2.0's organization branch judged the stored row (a system read), and 18.3.0's judged the caller's read of it; this keeps 18.3.0's not-found for a row the caller cannot read and takes the verdict from the stored row again.@objectstack/plugin-securitypatch, plus a one-sentence correction of a pending note (Acceptance notes, first item).Tests
org-admin-assignment-boundary.posture.test.ts(realObjectQLengine, SQL driver andSecurityPlugin), new describe "an organization administrator's removal is judged on the stored row, and its refusal names only values the caller can read", 11 tests:groupandisolated, for a position or permission set hidden by a field rule and for one partially masked: the caller reads the row without the stored value (precondition, off the read door); the removal of an in-boundary row is admitted and lands; the removal of an out-of-boundary row is refused403 PERMISSION_DENIEDby the boundary, and neither the stored values (the position, the sets behind it, the permission set) nor the masked value appear anywhere in the serialized answer;groupandisolated, for the row's organization hidden by a field rule and partially masked: the plugin's own D12 gate, asked as the write door asks it, admits the in-boundary removal on the stored organization, and refuses the out-of-boundary one naming the values the caller reads (the set) and not the organization, through the gate and through the door;groupandisolated: callers whose field rules hide a judged column still get the read door's not-found, whole, when they remove a row they cannot read or an id naming no row;single: a caller whose field rules hide the organization removes its own organization's in-boundary row end to end, and is refused another organization's row without that organization being named.delegated-admin-gate.test.ts, new top-level describe on the existing organization harness, 20 tests: for both administrator sets, each ofposition,permission_setandorganization_id(both tables) served hidden and masked, in-boundary removals resolve and out-of-boundary ones are refused with the rule's sentence and without the stored or masked value; a column the caller reads keeps its value in the guidance; a row the caller's read does not return and an id naming no row are the unreadable refusal, with no stored read of the addressed row.Acceptance notes
.changeset/org-admin-removal-caller-read.mdhas not shipped as a note (the plugin'sCHANGELOG.mdat 18.3.0 holds none of it). Its sentence "The delegated-administration gate (ADR-0090 D12) now judges that row as the administrator itself reads it, in its own context." is false after this change, so it now reads "now asks the administrator's own read, in its own context, whether that row is there to remove." Two of its bullets are corrected too: the not-found bullet now says "the removal of a row the administrator cannot read" (an edit in place is refused before any row is looked up, so the 404 is a removal's answer), and "A row the administrator can read keeps the answer it had, including the guidance…" now reads "A row the administrator can read is judged on the stored row. A refusal names the organization boundary, and names the row's organization, position or permission set only when the administrator's own read returns that value as stored." Its title, its first paragraph and its "Unchanged" bullet are true of the tree as written.check-empty-changesetnames the file and stays red by design until a maintainer confirms; restoring the sentence from the base would publish a false one.groupandisolated, and end to end undersingle. Under the walled postures a write also answers the write door's own row-level check, which this PR does not change; how that check treats a caller who does not read the organization column is a separate change the seat has ordered.update-by-id-refusal-order.dogfood.test.tspasses on this head (Verification).Verification
Readings at
d33de915d2, this body's head: the plugin typecheck, the whole suite, the two edited files, the three HTTP dogfood files and the derived gates. The ablations ran at5fd3195845; since then the gate file changed only by the merge of #30200 (its docblock and its update ordering). 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 two edited files
HTTP dogfood: the update-by-id refusal order and the two unreadable-row files
One-shot ablations of the two edited files (their subject resolves from this package's own source)
Derived gate families (dispatch-gates --commands: 60)
Contract-review findings commits 0cc6f17 and 0b0d0e2 (comments, describe names and the corrected pending note)
Generated by Claude Code