Repository navigation
fix(plugin-auth): a role change that grants an org-admin permission set names the set - #28086
Conversation
…et names the set
update-member-role now answers `outcome: 'permission_set_granted'` and
`grantedPermissionSet: { name, label }` beside the member row when the
change left the member holding an org-admin set they did not hold
before it. The before hook reads the pair's org-admin grants, the after
hook reads them again; both only read, so what is granted is unchanged.
The sys_member Change Role and Make Owner actions carry the outcome copy.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czy9koaxQiZdkGdPYY
…e beside its other copy Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czy9koaxQiZdkGdPYY
📓 Docs Drift CheckThis PR changes 2 package(s): 40 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 — 23 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 4f2e978b91c68134bb8d69bccec67cd0ae0b43f6 && git checkout 4f2e978b91c68134bb8d69bccec67cd0ae0b43f6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5448585d87246c0d57fdbbf422ece35c4c025241 fca3704b30db0f368af03995d4c790a1cd05e007 && git checkout -B drift-repro 5448585d87246c0d57fdbbf422ece35c4c025241 && git merge --no-ff fca3704b30db0f368af03995d4c790a1cd05e007
node scripts/docs-audit/affected-docs.mjs --json 5448585d87246c0d57fdbbf422ece35c4c025241
|
…ause-② line Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czy9koaxQiZdkGdPYY
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Contract-review-claim: The lane seat writes the |
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Contract reviewServed-tier: Owed on the declaration face alone; the diff touches no ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Seat ACCEPT · PR #28086 · head
|
Requested by Elon · project thread
Fixes #27862
Clause-②: no
Metadata-protocol: none
Skills-docs: none —
grep -rn "update-member-role\|permission_set_granted\|grantedPermissionSet\|organization_admin_no_bypass" skills content/docs/guides content/docs/protocolfinds no sentence about what a role change answers (one hit inskills/objectstack-platform/references/operations.md:201is about the first-adminadmin_full_accessbootstrap, a different grant).维护者速读
packages/plugins/plugin-auth/src/role-change-granted-set.ts(前后两次读授予、比出新增的那个);auth-manager-before-hook.ts/auth-manager-after-hook.ts的两处接入;sys-member.object.ts的两条outcomeMessages。pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/role-change-granted-set.test.ts -t 27862,预期 9 个用例通过。What changes
POST /api/v1/auth/organization/update-member-role(thesys_memberactions Change Role and Make Owner) now says which permission set the role change granted.role-change-granted-set.ts. The globalbeforehook reads the target member's org-admin grants (sys_user_permission_setrows namingorganization_adminororganization_admin_no_bypass, by name) and stashes them on the request's endpoint context, the__osPwChangeUserIdshape. The globalafterhook reads them again on a successful answer; a set held after and not before is named on the answer:outcome: 'permission_set_granted'andgrantedPermissionSet: { name, label }, the label read from the security catalog (the name when the definition carries none). This is the shape invite-member answersoutcomeby mail delivery andsys_invitationgains a servercopy_invitation_linkaction that answers the accept URL (ruling E on #27797) #27843 gave invite-member'soutcome.update_member_roleandtransfer_ownershipdeclareoutcomeMessages.permission_set_granted, naming${result.grantedPermissionSet.label}; the generated bundles carry the en leaf and hand-translated zh-CN, ja-JP and es-ES leaves (re-extracted, so no source-hash entry claims a copied leaf).plugin-securityis not in the diff. The new code calls onlyfind(the deps object has no write member); the grant is still written byreconcileOrgAdminGrantalone.afterhook leaves the answer as the vendor wrote it: a missing line, never a wrong one.Tests
packages/plugins/plugin-auth/src/role-change-granted-set.test.ts: a realAuthManagerover the installed better-auth, the role changed through the vendor's own route by the organization's owner. The grant is written by plugin-security's ownreconcileOrgAdminGrant, fired after eachsys_memberupdate as the security plugin's middleware fires it.outcome: 'permission_set_granted'andgrantedPermissionSet: { name: 'organization_admin_no_bypass', label: 'Organization Administrator (no record bypass)' }, the member row'sroleanduserIdintact, and the grant table holds exactly that one set.outcome/grantedPermissionSet; a plain member's attempt is refused 403 with neither, and nothing is granted.Acceptance notes
labelas written (English for the platform sets); the surrounding sentence is translated per locale.successMessageand Change Role's stay as they were for the no-grant case.Verification
Ablation (two, both reverted and verified by blob == HEAD)
auth-manager-after-hook.ts: thestampRoleChangeGrantedSetcall replaced byvoid roleChangeDeps;→Tests 3 failed | 6 passed: both (a) cases and the admin → owner case (its first step asserts the grant line) went red. Restored:git diff HEADempty.role-change-granted-set.ts:after.find((name) => !before.names.includes(name))→after.find(() => true)(ignore the before-snapshot) →Tests 1 failed | 8 passed: admin → owner went red. Restored:ok restored: blob == HEAD (ba71c8e10d19).Local gates (head a574295)
dispatch-gates --commands: 65 families derived; all 65 run with exit codes recorded;--ranreconciliation: "65 run, 0 NOT-MEASURED (a DERIVED zero — all 65 recorded an exit code and none of them is 3)".check:i18nandcheck:dual-build-cjs-loadsneed built output;check-plugin-teardown-shape --self-testneeds a commit the shallow clone lacked). Re-run after building the closure / fetching the commit: all exit 0.check:i18n: "OK (10 package(s) — all bundles in sync, no undeclared authoring keys)".a574295a:check:type-check-coverage,check:type-check-debt,check:test-source-alias,check:undeclared-dep-imports,check:cross-package-test-inputs,check:engine-double-contract,check:tier-file-adoption,check-issue-citations,check:i18n: all exit 0. The derived list is identical at both heads.pnpm --filter @objectstack/plugin-auth test: Test Files 205 passed (205), Tests 3904 passed / 10 skipped.typecheck: exit 0.pnpm --filter @objectstack/platform-objects testat head: Test Files 100 passed (100), Tests 1432 passed.typecheck: exit 0.pnpm lintnarrowed (declared):eslint --no-inline-configover the 9 changed.tsfiles, exit 0; the repo-wide run is CI's.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4czy9koaxQiZdkGdPYY
Generated by Claude Code