Repository navigation
feat(plugin-security,cli,spec)!: retire sys_user_permission_set.permission_set_id; the grant key is unique per organization; the boot grant-name backfill is deleted (C7b S3) - #23503
Conversation
…t_id retired; unique grant index on the name; the ceremony drops the column (ADR-0131 D4/D10, C7b S3) WIP: tests not yet run. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USccGXR13sWyTeEwe8QFdH
…y writer; fixtures drop the retired id; write-door pins Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…chor for the retired grant id Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…s 409 needs; re-measure the tenant-audit census Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
# Conflicts: # packages/cli/src/commands/migrate/organization-ownership.ts # packages/cli/src/utils/organization-ownership-apply.ts # packages/plugins/plugin-security/src/grant-permission-set-name-backfill.test.ts
…y; the plan counts grant keys without the name column; the retired indexes drop before the column - organization-ownership-grants.ts: one pure decision for --plan and --apply. A legacy grant whose derived name its user already holds in its organization (NULL keyed with NULL) is kept unnamed and reported with both ids; the rewrite blocks on it and deletes nothing. - --plan: sys_user_permission_set carries a grants block (duplicate groups, rewrite collisions); without the permission_set column it counts zero and names the boot, instead of refusing the whole plan (the inventory category is retired for it). - columnDrops: the port lives in organization-ownership-column-drops.ts and drops a column's retired indexes in an earlier, separate apply. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…s within one batch or multi-row write A batch insert or a multi-row update runs every row's before* hook before any row is written, so a read of the stored grants alone admitted two identical platform-wide rows of one call. The door now keeps the keys of one caller write in its dispatch.scope. Its 409 carries a cause naming what it saw. Kept in its own commit: if unique: 'organization' replaces the door check, this commit reverts cleanly. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…the unnamed-grant report leads with the ceremony; operators restart after --apply The changeset, the ADR-0087 entry, the object note and the ADR anchor say what the door enforces (stored grants and repeats within one write; not two concurrent single writes) and what the ceremony does with rewrite collisions and the retired indexes. The catalog reference report's remedy leads with running the ceremony: a grant re-written by name first is one the ceremony reports, not names. Two stale comments name the retired column and the deleted backfill no more. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…s repeats within one batch or multi-row write" Retires fix 2 (seat answers on #15211, Q-S3a): the grant key's index is respelled unique 'organization', whose NULL-safe key part refuses a second platform-wide grant for every writer, so the door's in-call check goes. This reverts commit bc15097. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…ation', and the door's platform-wide check retires Seat answers on #15211, Q-S3a: ADR-0120 D3 makes an 'organization' index's organization key part NULL-safe (COALESCE(organization_id, '__global__')), so the index itself refuses a second platform-wide grant, for every writer and write path (batches, multi-row updates, concurrent writes, raw SQL). The write door's duplicate check, its DuplicateRecordError and plugin-security's objectql dependency go; the pins now read the index's refusal, engine envelope and wire shape (409 UNIQUE_VIOLATION), including a batch insert and raw SQL beneath the engine. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
… nothing for a pair holding an unnamed grant in that organization Seat answers on #15211, Q-S3b (ii): a named grant written beside a 17.x grant the ceremony has not named yet would hold the very key the rewrite's rename needs. The unnamed grant confers nothing meanwhile, as every 17.x grant does until --apply; once named, the next sweep finds it. Pinned end to end in the ceremony fixture: the real sweep over a 17.x grant table carrying the declared index writes no row, and the rewrite then names the grant and completes. The rewrite's skip-and-report stays as the safety net for any other duplicate. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…r every writer; restate the changeset, the ADR-0087 entry and the anchor Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…ssertEngineDeleteDispatch; the pinned-double ledger learns it Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
…ant-column # Conflicts: # content/docs/permissions/tenant-audit-census.mdx # docs/audits/2026-08-tenant-audit-write-call-sites.counts.md # packages/plugins/plugin-security/src/catalog-reference-report.test.ts # packages/plugins/plugin-security/src/catalog-reference-report.ts # packages/plugins/plugin-security/src/grant-permission-set-name-backfill.test.ts
…eset's reconcile bullet moves out of the --plan list Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014fyKy7M7fEuwkfqc5JHdJJ
📓 Docs Drift CheckThis PR changes 5 package(s): 51 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 158 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 22453e93735bfcbbd0103a2b841ed922c369a94b && git checkout 22453e93735bfcbbd0103a2b841ed922c369a94b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 394f65e891f0ac6c6be708fb807740d5cb446520 7b50158db85fc41fb1b6a7c2b77f00be5f857651 && git checkout -B drift-repro 394f65e891f0ac6c6be708fb807740d5cb446520 && git merge --no-ff 7b50158db85fc41fb1b6a7c2b77f00be5f857651
node scripts/docs-audit/affected-docs.mjs --json 394f65e891f0ac6c6be708fb807740d5cb446520
|
Contract reviewServed-tier: Served on opus by the maintainer's order 「后续契约复审用 opus就够了」 (seat note 6166448512 on #6367). This re-reviews PR #23503 (branch
① Derived judgmentsBlocking fixes of 6166827307, as amended
Q-S3a: the index respelled
Q-S3b: answer (ii)
The
Whole-diff re-run of the rest
② Semver level
③ Boundary flagsSize: Tier H by AGENTS §7 (c).
Open questions and out-of-scope findings in the three reports
Composition with the sibling PRs
Landing order and merge obligations
If S1 reaches its record first, steps 2 and 3 swap, and the second lander carries the merge. Should S3 be tied to the joint S2 + S5 landing? No.
Non-blocking notes
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读 · PR #23503(#23430,C7b S3)·
|
…issue-23429-metadata-family One conflict: packages/rest/src/meta-delete-drop-storage-other-rows.test.ts — main parameterised the seed (seedStoredRow(packageId)); S2 drops the retired organization_id key from it. Took main's shape without the key. Auto-merged files both sides touch (main = S1/S3/#23489, kept as merged): organization-ownership.ts keeps S2's template-lookup column guard (S2's own hunk); organization-ownership-plan.ts and -inventory.ts are main's (S1) byte-for-byte; protocol.ts keeps #23489's packagelessReadAddress beside S2's legacy-row filtering; rest-server.ts keeps S2's deletion of the form doors' organization read; generated translations and the engine-double ledger are regenerated after the merge. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
Fixes #23430 · Part of #15211 (C7b stage S3; #15211 stays open for its other stages)
Clause-②: yes (narrowing)
What this does
Retires
sys_user_permission_set.permission_set_id(ADR-0131 D4/D10, C7b S3).plugin-security
permission_set_idand its(permission_set_id)index leave the grant object. The unique key moves to{ fields: ['user_id', 'permission_set', 'organization_id'], unique: 'organization' }. Its organization key part is NULL-safe (ADR-0120 D3,COALESCE(organization_id, '__global__')), so the index refuses a second platform-wide grant too, for every writer and every write path. This answers seat answer Q-S3a (feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211 6167037655), which retired an earlier write-door check. A write naming the retired column is refused400 INVALID_FIELD.grant-permission-set-name-backfill.ts, its anchor and itskernel:bootstrappedwiring are gone (Q4 → A). The v18 ceremony's verified rewrite replaces them.auto-org-admin-grant.ts: the id reach and the id dedupe are gone. Per Q-S3b (ii), the reconcile writes nothing for a pair that holds an unnamed grant in that organization.catalog-reference-report.ts:UnnamedGrant.permissionSetIdis gone. The remedy now leads with running the ceremony. This merges cleanly with 8-post's sixth class.bootstrap-platform-admin.ts: Leg C no longer checks for a legacy id. Any unnamed unscoped human grant withholds the promotion.grant-permission-set-name.tsis restated.cli:
os migrate organization-ownership(the v18 ceremony)column-dropslearns the named column (ColumnDrop {table, column, retiredIndexes}). It is gated on the physical column and runs afterrewrite, which reads the column by raw SQL. The two retired indexes are dropped first, in a separate, earlier schema-drift apply (organization-ownership-column-drops.ts). organization-ownership column-drops never completes where no platform-global objects are declared: organization_id is still injected on the sys_metadata family and sys_flow_dispatch, so the drop is never proposed #23399'snot-declaredoutcome carriescolumn.organization-ownership-grants.tsholds one decision shared by--planand--apply. The rewrite never names a legacy grant into a key its user already holds in its organization, with NULL keyed with NULL. Each such pair is reported with both ids, and nothing is deleted.--plan's grants block. It reports duplicate groups (which block the column drop) and rewrite collisions. On a database withoutpermission_setit counts zero and names "boot this release once" instead of refusing the plan.spec: one ADR-0087 column-retired semantic entry,
18.ups-permission-set-id-column-retired, after the precedent17.ups-delegated-from-column-retired.Files outside the claim's literal surface
The claim names
organization-ownership.tsonly. Q5 → A and the review's fixes also need these cli files:utils/organization-ownership-apply.ts(+ its test)utils/organization-ownership-plan.tsutils/organization-ownership-grants.ts(new)utils/organization-ownership-column-drops.ts(new, + its test)utils/organization-ownership-apply.served-catalog.integration.test.ts(one line)The other files outside it:
resolve-authz-context.ts).grant-name-pickerspin.scripts/engine-double-contract.pinned.json, which records the fixture's new engine shim.Verification
All runs below are on head
7b50158db8(mainmerged at0d3ea295d1):--project unit: 287 files, 4,307 passed · cli--project integration: 129 files, 1,125 passed, 2 skippedcheck:generated+vitest src/migrations: 209 passed · core typecheck: exit 0test/grant-name-pickers.dogfood.test.ts: 3 passednode scripts/pm/dispatch-gates.mjs --ran: 127 derived, 127 run, all exit 0, 0 NOT MEASUREDPins and their ablations. Each ablation went through
scripts/ablation-replace.mjs, and each restore matchedHEADwith an emptygit diff HEAD.unique: 'global'turned exactly the three platform-wide pins red: an insert by an admin and by a system writer, a two-row batch plus raw SQL beneath the engine, and a rename update. The org-scoped pin and the control stayed green.DUPLICATE_RECORD409, andmapDataErroranswers 409 with codeUNIQUE_VIOLATION.backfillOrgAdminGrantsruns over a 17.x-shaped grant table that carries the declared index. It writes no row,--plancounts no collision, and the rewrite names the grant and completes.--plancounts the pair,--applyblocks naming both ids and deletes nothing, and it finishes once the operator removes one grant. With the check disabled, the pin goes red.S5 ordering, read from S5's source and not run (
claude/issue-23429-boot-refusal@540c5b3c28). Onos serve, the boot guard (serve.ts:3880) is registered before the auto-registered SecurityPlugin (:4238), and neither depends on the other.kernel:readydispatch is propagating, so the guard's refusal stops the org-admin sweep. The index is still created at that boot.Acceptance notes
tryFindcontract, and the ceremony's skip-and-report covers any pair that results.--apply, no grant confers anything, the platform administrator's included. Restart the server after--apply.skills/objectstack-data/rules/security.mdstill says "a legacy column: do not write it". That is still right in effect, and editing it is Tier H, so it is left for a follow-up.维护者速读(草稿)
改了什么
sys_user_permission_set下线旧的permission_set_id列及其两个索引。user_id,permission_set,organization_id),并声明为unique: 'organization':组织为空(平台级)的重复授权也由索引直接拒绝。os migrate organization-ownership --apply)的已验证改写取代。--plan预先统计这些情况。为什么改
风险与代价(含回滚)
--apply之间,任何授权都不生效(包括平台管理员);--apply后需重启。permission_set_id写授权的种子或调用会收到400 INVALID_FIELD;重复授权收到409 UNIQUE_VIOLATION。席位意见
(留空,由席位定稿)
你要做的
Generated by Claude Code