Skip to content

fix(plugin-security): an organization administrator's update by id is refused before the row is looked up, whatever the id names - #30200

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/main-red-30198
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/main-red-30198

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Refs #30198

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:组织管理员(不持 adminScope)按 id 修改授权行时,恢复“先拒绝、不查行”的顺序:无论这个 id 指向能读到的行、读不到的行还是不存在的行,都回答同一个 403 PERMISSION_DENIED(“never edited in place”)。按 id 删除不变:读不到的行和不存在的 id 仍回答读取门的 404。
  • 为什么改: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 让组织管理员的 update 先问一次读取,不存在的 id 因此变成 404,打破了 update-by-id 拒绝顺序规则(不依赖行的拒绝要在查行之前做出),main 的 dogfood 分片随之变红(main red: Dogfood Regression Gate (2/3) fails update-by-id-refusal-order after PR #30165 #30198)。“就地编辑”本来就不在组织管理员的权限内,这个拒绝与行无关。
  • 风险与回滚:只删掉组织管理员分支 update 前的那次读取;删除、委派分支、兼持 adminScope 的组织管理员都不变;放行与拒绝的集合不变,只有这一类拒绝的状态码从 404 回到 403。回滚即撤销本 PR。
  • 先看:packages/plugins/plugin-security/src/delegated-admin-gate.ts 的 organizationAdminFailure(删去 update 分支的 hiddenFromCaller 调用,以及随之不再使用的这个方法);org-admin-assignment-boundary.posture.test.ts 里改指向的两处 update 断言。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/update-by-id-refusal-order.dogfood.test.ts,预期全部通过,组织管理员那条对照用例对行和不存在的 id 都是 403。
  • 需要你做的:无,席位落地。

What changes

  • An organization administrator's update by id is refused before the row is looked up, whatever the id names. In DelegatedAdminGate.organizationAdminFailure (ADR-0090 D12), the update branch no longer reads the row first. An edit in place is never part of the organization administrator's right, so that refusal does not depend on the row, and the update-by-id refusal order puts it before any lookup. A row, a row the administrator cannot read, and an id naming no row all get one 403 PERMISSION_DENIED with the same sentence and details. The private helper that only that read used is removed.
  • Unchanged. The read door's not-found still answers a removal by id of a row the administrator cannot read, and of an id naming no row (a verb the administrator holds, judged on the row). The delegated branch is untouched. An organization administrator who also holds an adminScope is still judged by the delegated walk, so its update of a row it cannot read still answers the not-found.

Tests

  • packages/qa/dogfood/test/update-by-id-refusal-order.dogfood.test.ts is unchanged. Its control "an organization administrator, who reads the platform grant object, keeps the administration gate's sentence for a row and for no row" is the case this restores.
  • Two pins that asserted the not-found for the organization administrator's update now assert the restored rule. Their removal halves are untouched:
    • org-admin-assignment-boundary.posture.test.ts:
      • under group and isolated, the update of a row the caller cannot read and of an id naming no row is one 403 PERMISSION_DENIED, compared whole, while the delete stays the read door's not-found, compared whole;
      • in the single control, the update of a row it reads and of an id naming no row is the same whole refusal.
    • org-admin-assignment-removal-unreadable-row.dogfood.test.ts: PATCH of the row behind the wall and of a missing id answers one 403, while DELETE stays equal to the GET of that id.
  • delegated-admin-assignment-write-unreadable-row.dogfood.test.ts is unchanged. Its organization administrator holds a scope, so it is judged by the delegated walk.

Acceptance notes

  • The unreleased changeset .changeset/assignment-write-unaddressable-row-not-found.md says that an organization administrator's edit in place "is asked the same question first". After this PR that sentence is no longer accurate: the edit is refused before any lookup. The new changeset states the rule. The old bullet is left as it stands, so that this fix carries no second changeset correction (which would need confirmation). It wants a one-line correction before the release that ships both.

Verification

Local readings (the plugin-security typecheck and suite, the posture rig, the three dogfood files, the derived gate families) are taken at this PR's head under the shared verify lock and reported to the seat. CI's families are this PR's own check-runs.


Generated by Claude Code

… refused before the row is looked up, whatever the id names

Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/delegated-administration.mdx (via DelegatedAdminGate (symbol, a top-level class))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via DelegatedAdminGate (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json dfafae81324e508cc666171d3385a15b89f31050 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 934e0bacd4e918a9c6b3bc11a05dbac3d013b0c0 — the merge of head a88b52d7d9072269f8eea2965aa1bd2b3d6d8226 into base dfafae81324e508cc666171d3385a15b89f31050, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 934e0bacd4e918a9c6b3bc11a05dbac3d013b0c0 && git checkout 934e0bacd4e918a9c6b3bc11a05dbac3d013b0c0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dfafae81324e508cc666171d3385a15b89f31050 a88b52d7d9072269f8eea2965aa1bd2b3d6d8226 && git checkout -B drift-repro dfafae81324e508cc666171d3385a15b89f31050 && git merge --no-ff a88b52d7d9072269f8eea2965aa1bd2b3d6d8226

node scripts/docs-audit/affected-docs.mjs --json dfafae81324e508cc666171d3385a15b89f31050

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs dfafae81324e508cc666171d3385a15b89f31050 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

席位核对

ACCEPT · head a88b52d7d9072269f8eea2965aa1bd2b3d6d8226 · 2026-11-05T02:26Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant