Repository navigation
Commit 5954705
fix(plugin-auth): a role change that grants an org-admin permission set names the set (#28086)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D8eFziY3iYNvFsFdazc7W6k)_
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/protocol` finds no sentence
about what a role change answers (one hit in
`skills/objectstack-platform/references/operations.md:201` is about the
first-admin `admin_full_access` bootstrap, a different grant).
## 维护者速读
-
改了什么:把成员角色改成管理员或所有者时,接口的返回里现在会说明这次顺带授予了哪个权限集(名称和显示名),控制台的提示也会写出来,比如「成员角色已更新。新角色还授予了权限集
Organization Administrator (no record bypass)。」
-
为什么改:改角色会自动给这个人加上组织管理员权限集,但提示只写「成员角色已更新」,管理员事后在用户的权限集页签上看到一个没人手动分配的权限集,不知道从哪来的。
- 风险与回滚:只加读不加写,授予什么完全不变(授予仍由 plugin-security
的协调器单独完成);读失败时就不显示这句,不会显示错的。回滚即 revert 本 PR。
-
先看:`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` (the `sys_member`
actions Change Role and Make Owner) now says which permission set the
role change granted.
- **plugin-auth, new `role-change-granted-set.ts`.** The global `before`
hook reads the target member's org-admin grants
(`sys_user_permission_set` rows naming `organization_admin` or
`organization_admin_no_bypass`, by name) and stashes them on the
request's endpoint context, the `__osPwChangeUserId` shape. The global
`after` hook reads them again on a successful answer; a set held after
and not before is named on the answer: `outcome:
'permission_set_granted'` and `grantedPermissionSet: { name, label }`,
the label read from the security catalog (the name when the definition
carries none). This is the shape #27843 gave invite-member's `outcome`.
- **platform-objects.** `update_member_role` and `transfer_ownership`
declare `outcomeMessages.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).
- **Purely additive.** The member row's own columns are untouched; a
role change that grants nothing (admin to owner keeps the set already
held, a demotion revokes it) and every refusal answer exactly what they
answered before. No new error code, no spec key.
- **What is granted is unchanged.** `plugin-security` is not in the
diff. The new code calls only `find` (the deps object has no write
member); the grant is still written by `reconcileOrgAdminGrant` alone.
- **Fails toward silence.** A read that throws, a member id that
resolves to no row, or a stash that did not reach the `after` hook
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
real `AuthManager` over 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 own `reconcileOrgAdminGrant`, fired after
each `sys_member` update as the security plugin's middleware fires it.
- (a) member → admin and member → owner answer `outcome:
'permission_set_granted'` and `grantedPermissionSet: { name:
'organization_admin_no_bypass', label: 'Organization Administrator (no
record bypass)' }`, the member row's `role` and `userId` intact, and the
grant table holds exactly that one set.
- (b) admin → owner (set already held) and admin → member (revoked)
answer no `outcome` / `grantedPermissionSet`; a plain member's attempt
is refused 403 with neither, and nothing is granted.
- Fail-toward-silence units: a before-read that throws, an after-read
that throws, a set with no catalog label (named by its name), another
route.
## Acceptance notes
- The label is the catalog definition's `label` as written (English for
the platform sets); the surrounding sentence is translated per locale.
- Make Owner's `successMessage` and Change Role's stay as they were for
the no-grant case.
## Verification
<details><summary>Ablation (two, both reverted and verified by blob ==
HEAD)</summary>
1. `auth-manager-after-hook.ts`: the `stampRoleChangeGrantedSet` call
replaced by `void 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 HEAD` empty.
2. `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 (ba71c8e)`.
</details>
<details><summary>Local gates (head a574295)</summary>
- `dispatch-gates --commands`: 65 families derived; all 65 run with exit
codes recorded; `--ran` reconciliation: "65 run, 0 NOT-MEASURED (a
DERIVED zero — all 65 recorded an exit code and none of them is 3)".
- Three families first answered exit 3 (PREREQUISITE NOT MET:
`check:i18n` and `check:dual-build-cjs-loads` need built output;
`check-plugin-teardown-shape --self-test` needs 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)".
- After the second commit (a test-file edit) the families that read test
files were re-run on head `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 test` at head: Test Files
100 passed (100), Tests 1432 passed. `typecheck`: exit 0.
- `pnpm lint` narrowed (declared): `eslint --no-inline-config` over the
9 changed `.ts` files, exit 0; the repo-wide run is CI's.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4czy9koaxQiZdkGdPYY
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4czy9koaxQiZdkGdPYY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6410029 commit 5954705
13 files changed
Lines changed: 520 additions & 10 deletions
File tree
- .changeset
- packages
- platform-objects/src
- apps/translations
- identity
- plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
694 | 694 | | |
695 | 695 | | |
696 | 696 | | |
697 | | - | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
698 | 701 | | |
699 | 702 | | |
700 | 703 | | |
| |||
704 | 707 | | |
705 | 708 | | |
706 | 709 | | |
707 | | - | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
708 | 714 | | |
709 | 715 | | |
710 | 716 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
694 | 694 | | |
695 | 695 | | |
696 | 696 | | |
697 | | - | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
698 | 701 | | |
699 | 702 | | |
700 | 703 | | |
| |||
704 | 707 | | |
705 | 708 | | |
706 | 709 | | |
707 | | - | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
708 | 714 | | |
709 | 715 | | |
710 | 716 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
694 | 694 | | |
695 | 695 | | |
696 | 696 | | |
697 | | - | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
698 | 701 | | |
699 | 702 | | |
700 | 703 | | |
| |||
704 | 707 | | |
705 | 708 | | |
706 | 709 | | |
707 | | - | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
708 | 714 | | |
709 | 715 | | |
710 | 716 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
694 | 694 | | |
695 | 695 | | |
696 | 696 | | |
697 | | - | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
698 | 701 | | |
699 | 702 | | |
700 | 703 | | |
| |||
704 | 707 | | |
705 | 708 | | |
706 | 709 | | |
707 | | - | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
708 | 714 | | |
709 | 715 | | |
710 | 716 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
215 | 223 | | |
216 | 224 | | |
217 | 225 | | |
| |||
271 | 279 | | |
272 | 280 | | |
273 | 281 | | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
274 | 287 | | |
275 | 288 | | |
276 | 289 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
74 | | - | |
| 75 | + | |
75 | 76 | | |
76 | 77 | | |
77 | 78 | | |
| |||
93 | 94 | | |
94 | 95 | | |
95 | 96 | | |
96 | | - | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
97 | 103 | | |
98 | 104 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
| |||
64 | 65 | | |
65 | 66 | | |
66 | 67 | | |
| 68 | + | |
67 | 69 | | |
68 | 70 | | |
69 | 71 | | |
| |||
232 | 234 | | |
233 | 235 | | |
234 | 236 | | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
235 | 245 | | |
236 | 246 | | |
237 | 247 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| 121 | + | |
121 | 122 | | |
122 | 123 | | |
123 | 124 | | |
| |||
706 | 707 | | |
707 | 708 | | |
708 | 709 | | |
| 710 | + | |
709 | 711 | | |
710 | 712 | | |
711 | 713 | | |
| |||
720 | 722 | | |
721 | 723 | | |
722 | 724 | | |
| 725 | + | |
723 | 726 | | |
724 | 727 | | |
725 | 728 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| 74 | + | |
74 | 75 | | |
75 | 76 | | |
76 | 77 | | |
| |||
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
| 102 | + | |
101 | 103 | | |
102 | 104 | | |
103 | 105 | | |
| |||
660 | 662 | | |
661 | 663 | | |
662 | 664 | | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
663 | 674 | | |
664 | 675 | | |
665 | 676 | | |
| |||
0 commit comments