Repository navigation
fix(plugin-security): the delegated walk counts the permission set of the position's own name - #24793
Conversation
… the position's own name ADR-0090 D12 as amended 2026-10-17: the sets a position carries are every set an assignment of it grants, the own-name set included while the resolver folds held position names into the requested set names. The delegated branch's setsBoundToPosition now reads setsCarriedByPosition, the helper the organization-level branch already used, so the allowlist check, strict containment, the self-delegation rule against handing out administration and the delegable-scope listing all judge that set. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
…name-set Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
📓 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 — 15 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 0bdd048947982c6ecb0c0e6aa6e6af4eaf07d8c8 && git checkout 0bdd048947982c6ecb0c0e6aa6e6af4eaf07d8c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 46bb5e3e5405fad31ccba6fe571e98b9234aac78 d110c151168562f35123475bc12338f9118261f8 && git checkout -B drift-repro 46bb5e3e5405fad31ccba6fe571e98b9234aac78 && git merge --no-ff d110c151168562f35123475bc12338f9118261f8
node scripts/docs-audit/affected-docs.mjs --json 46bb5e3e5405fad31ccba6fe571e98b9234aac78
|
席位核对 · ACCEPT ·
|
Fixes #24736
Clause-②: no
What changes
The delegated-administration gate's position walk,
setsBoundToPosition, now takes its set names fromsetsCarriedByPosition. That is the helper the organization-level branch already reads. So a delegate's position assignment is judged on every permission set the assignment grants. ADR-0090 D12, as amended on 2026-10-17, lists them: the names the position's definition lists and, while the resolver folds held position names into the requested set names, the permission set of the position's own name when the catalog holds one.The walk has three callers on
main, and all three now judge that set:assertAssignmentWritehandles asys_user_positioninsert, update or delete by anadminScopeholder. Both the allowlist check and strict containment now see the own-name set.describeDelegableScope, delegated branch: a position whose own-name set is outside every placing scope's allowlist is no longer offered.GET /api/v1/security/my-delegable-scopenow agrees with the write.assertSelfDelegationrule 6 (ADR-0091 D3): a delegatable position whose own-name set carries anadminScopecan no longer be self-delegated.A refusal names the set and says it is the set of the position's own name, because the position's definition need not list it. The envelope is unchanged:
403 PERMISSION_DENIED.Not changed: the organization-level branch, tenant-level administrators, the resolver's fold in
security-plugin.ts,packages/spec, and the shipped permission sets. The change only narrows what a delegate may do. Nothing a delegate was refused before is admitted now.Reachability, measured on
origin/main795ff708d8packages/**andexamples/**, test fixtures excluded. Positions: the six built-ins (builtin-positions.ts), three inapp-crmand ten inapp-showcase, 19 in all. Permission sets: the platform's eight (default-permission-sets.ts), two inapp-crmand nine inapp-showcase, all prefixedshowcase_. The two name lists share 0 names.packages/metadata/src/__fixtures__/hotcrm-17.1-built-permissions.artifact.json, carries the permission setssales_managerandsales_rep. HotCRM declares positions of the same names, and Setup: a position's same-named permission set is in force but invisible — the set's Assigned Users panel is empty and the position reads as granting nothing #24757 measured that set in force through the fold on a booted HotCRM.200in all three postures (single,group,isolated). The suite's ground-truth case shows the assignee's resolved sets then include the own-name set; that case is green before and after. The same held for an own-name set that carries anadminScope, and the delegable-scope report offered both positions.The triage note on this card says a reachable shape makes it p1.
Tests, at head
d110c15116The branch merged
origin/main7d45c8e29a, which touchesplugin-security's shipped sets. The closure was rebuilt and the package run again on the merged head:pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2:Test Files 192 passed (192),Tests 3995 passed | 45 skipped (4040). Before the merge, ata3fc36f503:191 passed (191),3961 passed | 45 skipped.pnpm --filter @objectstack/plugin-security typecheck: exit 0 at both heads. Both new test files are in thetsconfig.test.jsonprogram;tsc -p tsconfig.test.json --listFilesOnlylists both.pnpm --filter @objectstack/plugin-security build: exit 0.src/delegated-admin-gate.test.ts, in the describe block on the permission set of the position's own name:codePERMISSION_DENIEDandstatus403 that names the set;adminScope;src/delegated-admin-own-name-set.posture.test.ts: a realObjectQL,SqlDriverandSecurityPluginin three postures. Each posture runs the ground truth, the 403 refusal naming the set, the control, the unchanged no-own-name case, strict containment, and the listing compared with the write.Ablation
Taken at
a3fc36f503, before the merge, which did not touch the gate or its tests. The change was committed first. Then the old walk, which readposition.definition.permissionSetsonly, was put back throughscripts/ablation-replace.mjs: anchor 1 to 0, blob2a202d1153e1to2e45318f6351. Both files were run:Tests 14 failed | 125 passed (139).Restored with
git checkout HEAD -- PATH: the blob is back to2a202d1153e1, the same as HEAD,git diff HEADis empty, and the marker count is 0. The tests import the subject through relative source paths (./delegated-admin-gate,./security-plugin.js), not adist/, so the ablation needed no rebuild.Gates
Local subset, at head
d110c15116:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 67 commands from the merge base7d45c8e29a. All 67 were run one by one, each exit captured before any pipe, and all 67 exited 0.--ranprinted:Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.On the first head (
a3fc36f503),check:dual-build-cjs-loadsandcheck:i18nrefused withPREREQUISITE NOT MET(exit 3): nothing was measured. After the build thatcheck:i18nnames,check:i18npassed on that head, and both passed in the battery on the merged head.Every family CI runs is read from this PR's check-runs. None of them was run again here.
Acceptance notes
main: the binding authoring rows retired with ADR-0131 D3. The walk's three callers are the ones listed above.setsCarriedByPosition).setsBoundToPositiononly adds each set'sadminScope.describeDelegableScopeoffers the four built-in identity positions (platform_admin,org_owner,org_admin,org_member). The write refuses each of them for every writer with400 VALIDATION_FAILED. This was measured on the real stack in all three postures. The organization-level branch already skips them (isReservedIdentityName).Generated by Claude Code