Repository navigation
feat(core,plugin-security,plugin-auth)!: the authorization resolver reads a grant's permission set by name (ADR-0131 D4, C2 stage S5a) - #22495
Conversation
…s per principal on the base Recorded with production code at the base: platform administrator, organization administrator, member and agent in single, group and isolated, the envelope inside the organization and outside it, and platform standing. Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…n set by name resolveUserAuthzGrants §6 keys a sys_user_permission_set grant on its permission_set name (ADR-0131 D4). The set row is the grant's own organization's row, else the organization-less one; a grant that names nothing, or whose name resolves only to another organization's set, confers nothing. The platform-admin anchor reads the organization-less admin_full_access an unscoped grant names. Position-bound sets are still reached through the junction's id (ADR-0131 C3). The admin-standing surface declares the columns now read. The core test fixtures' grants carry the name the platform writers store; the batch-equivalence query goldens move for the sys_permission_set reads only. Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…zations; the guard reads the anchor the resolver reads The grant name hook applies the grant rule at write time: the set row the id points at must be the grant's own organization's or organization-less. Another organization's set (readable by a tenant-less system writer) is never named after: a supplied name is refused from every caller, no name is stamped, and a re-point or an organization move clears the name. The break-glass guard counts the organization-less admin_full_access row as the anchor, as the resolver now does, and judges writes to its organization_id. Pins for the resolver's by-name read in core. Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…ite-time organization rule, the upgrade boot; changeset Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…ion is a standing write Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…s as the platform writers do Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
… do; the memory doubles read an absent column as NULL Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…'s name; doubles answer the by-name read The authorization resolver reads a user grant's set by name, so the fixtures' grant rows carry the name every platform writer stores beside the id, and the hand-written engine doubles read an absent column as NULL and answer a sys_permission_set read by name. Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…t fixtures carry their set's name; doubles answer the by-name read Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
… before the stored name Claude-Session: https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8 Co-authored-by: Claude <noreply@anthropic.com>
…a-resolver-grant-name
📓 Docs Drift CheckThis PR changes 4 package(s): 27 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 — 43 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 8952586cb17ebc454483a8bc495746fdd0330c64 && git checkout 8952586cb17ebc454483a8bc495746fdd0330c64
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 35ef501e1301a2e992f4f785a9ef469ce79fc086 2e2d3bec0d43d273647138c1a468b1c9acaa15de && git checkout -B drift-repro 35ef501e1301a2e992f4f785a9ef469ce79fc086 && git merge --no-ff 2e2d3bec0d43d273647138c1a468b1c9acaa15de
node scripts/docs-audit/affected-docs.mjs --json 35ef501e1301a2e992f4f785a9ef469ce79fc086
|
Part of #15196
Clause-②: no (narrowing)
C2 stage S5a (
domain:engine): the authorization resolver reads which permission set a user grant holds from the grant's name,sys_user_permission_set.permission_set(ADR-0131 D4). The card stays open for S5c, S8, S9 and S10.What changes
The resolver (
packages/core/src/security/resolve-authz-context.ts,resolveUserAuthzGrants§6 and §6b):readGrantSetRowsByName). The grant'spermission_set_idis no longer read here.admin_full_accessrow only. Deactivation is still read from the row. Under walled postures the anchor stays retired.KEPT-C3, untouched).sys_positionread, so it adds no sequential leg.ADMIN_STANDING_SURFACEdeclares the columns the resolver now reads:sys_user_permission_set.permission_setin place of the id, andsys_permission_set.organization_id.Prerequisite 2: the S4a name hook applies the S4b rule at write time (
grant-permission-set-name.ts).qa_b_only, that set's name.400 VALIDATION_FAILEDandinvalid_valueatpermission_set;Prerequisite 1: the upgrade-boot window, accounted for in S5a.
kernel:bootstrapped. Both kernels await everykernel:bootstrappedhandler beforekernel:listening, where HTTP servers open their socket. So no request reaches the resolver while the grants the backfill can name are still unnamed.kernel:readysees them unnamed. Pinned on a realLiteKernelboot.kernel:readyreaders) was not taken. S4b measured that akernel:readyplacement misses late-registered sets (its ablation A6). Under a by-name resolver those grants would then confer nothing for the whole process lifetime.security-plugin.tsis not touched.The S4b carrier: a grant whose id names another organization's set confers nothing. This follows the seat's ruling: a set resolves by name, in its own organization's row and otherwise the organization-less row.
Break-glass guard (
plugin-auth,last-admin-guard.ts). The resolver now readssys_permission_set.organization_id, so the guard's correspondence gate asks for a disposition. The guard now:admin_full_accessrow as the anchor, matching the resolver;Read sites (census rows 31–35, located by symbol on
35ef501e1)sys_user_positionread (Leg 1)ASSIGN)sys_user_permission_setread (Leg 1)permission_set_id→permission_setREWRITE-C2sys_positionread (§6a)activeKEPT-C3id bridge,OPEN-ACTIVE;REGis S8asys_position_permission_setread (§6a)KEPT-C3sys_permission_setread (§6b)REWRITE-C2and the S4b carrierKEPT-C3(id→name bridge)active, capability bodyOPEN-ACTIVE);REG(body)is S8aadmin_full_accessgrantunscopedUserPsIds)platform-admin.ts(§6b-config)@objectstack/corecannot importplugin-security. It keeps its own internal copy of the rule (grantSetNameOf, not exported), the shape S5b set ("one copy per package, internal"). No new dependency edge, no cycle.Pins, each reverse-verified (one-time runs, quoted in the report)
resolve-authz-grant-set-by-name.golden.test.tscovers the platform admin, the organization admin, a member and an agent, insingle,groupandisolated. Each records the envelope inside the organization and outside it, plushasPlatformAdminStanding.61765bfb: goldens only, production code at the base.ablation-replace.single1/3 and walled 2/3, platform adminsingle1/3 (no grant row under a wall).LiteKernelboot, a grant stored before the name column is unnamed atkernel:readyand confers atkernel:listening.kernel:readyleg is red on the base resolver.kernel:listeningleg red.admin_full_accessinto an organization is refused. Red with the guard at the base; the two controls stay green.Fixture triage
The resolver no longer reads the id, so test grants written without the name hooks now carry the name every platform writer stores beside the id. Hand-written engine doubles now read an absent column as NULL, as SQL does, and answer a
sys_permission_setread by name.sys_permission_setentries of the three fixtures that hold a user grant. The move is written down in the test file, as its header requires.Cross-lane paths (
domain:services, beyond the claim's file surface)packages/plugins/plugin-security/src/grant-permission-set-name.ts, as claimed.packages/plugins/plugin-auth/src/last-admin-guard.ts, not in the claim. This is the guard's half of the surface this stage changed: its correspondence gate is red without it.grant-permission-set-name.test.ts,grant-permission-set-name-backfill.test.ts,resolve-authz-grant-set-by-name.test.tsandresolve-authz-grant-set-by-name.golden.test.ts(both new), and the fixtures ofexplain-enforce-parity,explain-engine,explain-positions-name-authority,explain-removed-member-principal,orgless-position-name-foldandsecurity-plugin.last-admin-guard.test.tsand the fixtures of 10 test files.security-plugin.ts, nopackages/spec, no id-column change, no deletion, no driver arm, no junction read.Verification at
2e2d3becorigin/main35ef501e1was merged in clean.dispatch-gates --commandsderived 71; all 71 exit 0.--rananswers "71 derived, 71 run, 0 NOT-MEASURED (a DERIVED zero)". Two needed a prerequisite before they ran: a fixture commit fetched forcheck-plugin-teardown-shape --self-test, and seven unrelated packages built forcheck:dual-build-cjs-loads.check:durability-log-level: exit 0.eslint --no-inline-config --format jsonover the 73 changed TS files: 73 linted, 0 errors, 0 warnings, none ignored.eslint.config.mjsenables no type-aware linting, so untouched files are invariant.cliintegration test (a write through the real data door, stamped by the hook) andhttp-conformanceare left to CI.Acceptance notes
security-plugin.tsstill says unnamed grants "keep no name until a later boot runs it". With this stage they also confer nothing until then.security-plugin.ts. Not edited here.Generated by Claude Code