Repository navigation
Commit 8c9260f
feat(plugin-auth): an SSO sign-in grants the permission set its IdP group is mapped to (#31657)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi98qy3EtdY1M87bPE16diw6k)_
Fixes #30967
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:企业 SSO(OIDC / SAML)登录时,IdP 发来的组会按 `sys_idp_group_mapping`
映射授予普通权限集,和 SCIM 共用同一个映射对象、同一道保存门和同一套边界判断;平台管理员用现有的 `set-group-mapping`
门加 `providerId` 保存映射。
- 为什么改:#30967 第 3 项(最后一项),按裁决 A(记录 6718355623)「同一个映射对象,不建第二套模型」,在 #30959
已落地的映射上补 SSO 一半。
- 风险与回滚:只放宽「组 →
普通权限集」;管理员集与组织边界外的集在保存和每次授予时都被拒,没有安全服务时一律不授予;登录不会因为映射失败而失败(只记 error
日志)。回滚即 revert 本 PR,已授予的行带 `sso-group-projection` 标记,可按标记清理。
-
先看:`packages/plugins/plugin-auth/src/sso-group-projection.ts`(登录时读组、匹配、调用共享对账)、`packages/plugins/plugin-auth/src/idp-group-mapping-admin.ts`(`providerId`
分支与删除即撤销)、`packages/plugins/plugin-auth/src/scim-group-projection.ts`(抽出共享的准入与对账,SCIM
行为不变)。
- 两分钟验证:`cd packages/plugins/plugin-auth && pnpm exec vitest run
src/sso-oidc-refusal-cause.test.ts -t "IdP group is mapped"`,预期 10
个用例全绿。
- 需要你做的:无,席位落地。
## What changes
Item 3 of #30967 (IdP group → permission set on SSO sign-in), the last
open item, under maintainer ruling A (record 6718355623): it follows
#30959's ruled model, the same `sys_idp_group_mapping` object, no second
model. Built on what PR #31443 landed; nothing in it is duplicated.
- **Shared projection, two sources** (`scim-group-projection.ts`). The
boundary admission and the reconcile are lifted into
`createMappingAdmission(deps, kind)` and `reconcileMappedGrants(engine,
{ userId, reason, mappingIds }, admits)`; the SCIM projection now calls
them (its own behaviour, refusal text and marker are unchanged, and its
suite passes unchanged). A `sourceMappings(engine, kind, sourceId)` read
and an SSO marker `ssoGroupProjectionReason(providerId)`
(`sso-group-projection: provider 'ID'`) are added.
- **The sign-in hook** (`sso-group-projection.ts`, wired in
`auth-manager-plugin-list.ts`). `sso({ provisionUser,
provisionUserOnEveryLogin: true })`: every sign-in reads
`userInfo.groups`, which the provider's own `mapping.extraFields.groups`
fills (a string or a list), matches each value against the provider's
rows with the SCIM half's matcher (`group_external_id` when the row
names one, else exact `group_display_name`), and reconciles the user's
grants carrying this provider's marker. A sign-in with no groups desires
nothing: it removes only grants this provider made. Writes go through
the engine, so the break-glass guard judges them. A failure never fails
the sign-in; it is logged at `error` once, naming the consequence and
the remedy.
- **The save door** (`idp-group-mapping-admin.ts`). `POST
/admin/scim/set-group-mapping` accepts `providerId` in place of
`connectionId` (exactly one). The organization is the provider row's
`organization_id`; unknown provider → `reference_not_found`, provider
with no organization → `rule_violation`, both on `providerId`; SSO off →
`503`. The same boundary judgement and the same `503` without a security
service. An SSO save applies at each member's next sign-in
(`reprojected: false`). `delete-group-mapping` is source-aware: deleting
an SSO mapping removes at once the grants its provider made of that set
in that organization, unless another of its mappings still names it.
- **The object** (`platform-objects`).
`sys_idp_group_mapping.source_kind` gains the option `sso` (`source_id`
= the provider id); three field descriptions name both sources;
translations for all nine locales (the `en` bundle regenerated, the
eight others translated by hand).
- **Route ledger.** The set row's note describes both sources and drops
`requires: 'scim'` (the mount is unconditional, and the door now serves
SSO with SCIM off); the delete row's note describes the SSO revoke.
- **Tenant-audit census**
(`content/docs/permissions/tenant-audit-census.mdx`,
`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`). The
delete-revoke is one new engine write site (275 → 276); the generated
regions are regenerated and the page's prose figures updated to the
census. Gate-required, not a behaviour change.
## Tests
New top-level describe in the existing OIDC SSO harness,
`sso-oidc-refusal-cause.test.ts` ("an SSO sign-in grants the permission
set its IdP group is mapped to"): the real `AuthManager` with the real
`sso()` plugin, driven `/sign-in/sso` → a stubbed IdP → `/sso/callback`,
on a real ObjectQL engine over better-sqlite3 with the break-glass guard
registered, the mapping saved through the door's own function, judged by
plugin-security's real `organizationBoundaryFailure`.
1. A mapped group grants the set in the provider's organization, by
display name and by external id (a list claim and a string claim).
2. Leaving the group revokes the projected grant at the next sign-in; a
grant made by hand stays.
3. CONTROL: an unmapped group grants nothing.
4. CONTROL: a sign-in with no groups claim lands as before, writes
nothing to the grant table and logs nothing.
5. A mapping naming an administrator set, or a set outside the
organization boundary, is refused at save (`400 VALIDATION_FAILED`,
`permissionSet`, `rule_violation`) and stores nothing.
6. A set redefined outside the boundary after its mapping was saved
grants nothing at sign-in.
7. With no security service, the save is refused `503`, and a stored
mapping grants nothing at sign-in (fail closed).
8. The break-glass guard still refuses through the sign-in: the last
administrator's grant is not removed, and the sign-in still lands (one
`error` line).
9. Deleting the mapping removes the grants it projected at once.
10. The door names exactly one source; an SSO source must be a provider
that belongs to an organization.
Forced test edits: none. The only edit to existing harness code is
additive: `TokenShape` gains an optional `claims` member that
`idToken()` spreads into the signed claims.
## Acceptance notes
- Group membership changes reach the platform only at a sign-in: an SSO
provider pushes nothing between sign-ins, so a user removed from an IdP
group keeps the set until their next sign-in through that provider (SCIM
is the push channel for prompt revocation). A deleted mapping's grants
are removed at once.
- An absent groups claim is read as "no groups": the provider's own
earlier grants are removed. A provider whose mapping does not fill
`groups` therefore never grants and never removes anything but its own
marked rows, of which it has none.
- A set held through any other row (by hand, by SCIM) is not written
again; whichever source wrote first owns the row.
- The door's path keeps the `scim` segment while now serving both
sources (seat ruling: no `/admin/sso/*` mounts); listed in open
questions as a naming note.
- `content/docs/permissions/sso.mdx` does not yet describe group mapping
(outside this card's file surface); the changeset and the route-ledger
notes carry the operator-facing text.
- SAML: the vendor fills `userInfo` from `mapping.extraFields` the same
way (SAML attribute values may be lists, which the reader takes); the
end-to-end pins drive OIDC only.
## Verification
Head: `70db6d0601c6ca0f3d64750fcd93c9ce95b87a19` (origin/main `8784ccbc`
merged in, no rebase).
<details><summary>Red first on main</summary>
The new describe run against the base before any source change: 10 of 10
red (the door refused `providerId` as an unknown field, and no sign-in
granted anything).
</details>
<details><summary>Tests on the head</summary>
- `pnpm exec vitest run src/sso-oidc-refusal-cause.test.ts`
(plugin-auth): 1 file, 32 passed.
- plugin-auth full suite under the verify lock: Test Files 244 passed
(244), Tests 4372 passed, 10 skipped.
- platform-objects full suite: Test Files 120 passed, Tests 1886 passed.
- `tsc --noEmit` for plugin-auth and platform-objects: exit 0. `eslint
--no-inline-config` on both packages: exit 0.
- `pnpm install --frozen-lockfile && pnpm build` under the verify lock:
exit 0.
</details>
<details><summary>Ablation: each pin goes red when its behaviour is
removed</summary>
Each mutation was applied on disk to the source, the describe run, then
restored (`git diff HEAD` empty after each).
| Mutation | Red tests |
| :--- | ---: |
| group matcher always matches | 3 |
| sign-in admission always admits | 2 |
| save-time boundary refusal removed | 1 |
| save-time fail-closed (no security service) removed | 1 |
| reconcile removes grants outside the engine's guard | 1 |
| a projection failure fails the sign-in | 1 |
| reconcile ignores the provider marker | 1 |
| `provisionUserOnEveryLogin` off | 2 |
| delete does not remove the projected grants | 1 |
One first run was a no-op (its replacement still contained the anchor,
so nothing changed); it was re-run with a mutation that moved the
anchor, which went red. It is the "a projection failure fails the
sign-in" row.
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands` on the head derived 108
commands; all 108 ran with exit 0, and `--ran` reconciled: 108 derived,
108 run, 0 NOT-MEASURED, 0 UNRUN. On the first pass
`check-tenant-audit-census` found the new delete-revoke write without a
tenant context: it now carries the grant's organization as its tenant,
and the census page and ledger counts are regenerated
(`tenant-audit-census.mjs --write`) with the page's prose figures
updated to match (276 sites; the provable no-context figure stays 3).
`check:i18n` and `check:i18n-stale-fill` pass with the regenerated `en`
bundle.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmNwrm1hJ9e7eLARM7n)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6c6cfcc commit 8c9260f
21 files changed
Lines changed: 882 additions & 147 deletions
File tree
- .changeset
- content/docs/permissions
- docs/audits
- 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 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
| 135 | + | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| |||
197 | 197 | | |
198 | 198 | | |
199 | 199 | | |
200 | | - | |
| 200 | + | |
201 | 201 | | |
202 | | - | |
203 | | - | |
| 202 | + | |
| 203 | + | |
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
| |||
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
214 | | - | |
| 214 | + | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
220 | | - | |
| 220 | + | |
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
224 | | - | |
| 224 | + | |
225 | 225 | | |
226 | 226 | | |
227 | 227 | | |
| |||
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
237 | | - | |
| 236 | + | |
| 237 | + | |
238 | 238 | | |
239 | | - | |
| 239 | + | |
240 | 240 | | |
241 | | - | |
| 241 | + | |
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
246 | | - | |
| 246 | + | |
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
| 252 | + | |
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | | - | |
| 258 | + | |
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
| 310 | + | |
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
315 | | - | |
| 314 | + | |
| 315 | + | |
316 | 316 | | |
317 | 317 | | |
318 | 318 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
98 | | - | |
| 97 | + | |
| 98 | + | |
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
| 161 | + | |
161 | 162 | | |
162 | 163 | | |
163 | 164 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2660 | 2660 | | |
2661 | 2661 | | |
2662 | 2662 | | |
2663 | | - | |
| 2663 | + | |
2664 | 2664 | | |
2665 | | - | |
| 2665 | + | |
| 2666 | + | |
2666 | 2667 | | |
2667 | 2668 | | |
2668 | 2669 | | |
2669 | 2670 | | |
2670 | | - | |
| 2671 | + | |
2671 | 2672 | | |
2672 | 2673 | | |
2673 | 2674 | | |
| |||
2683 | 2684 | | |
2684 | 2685 | | |
2685 | 2686 | | |
2686 | | - | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2660 | 2660 | | |
2661 | 2661 | | |
2662 | 2662 | | |
2663 | | - | |
| 2663 | + | |
2664 | 2664 | | |
2665 | | - | |
| 2665 | + | |
| 2666 | + | |
2666 | 2667 | | |
2667 | 2668 | | |
2668 | 2669 | | |
2669 | 2670 | | |
2670 | | - | |
| 2671 | + | |
2671 | 2672 | | |
2672 | 2673 | | |
2673 | 2674 | | |
| |||
2683 | 2684 | | |
2684 | 2685 | | |
2685 | 2686 | | |
2686 | | - | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2660 | 2660 | | |
2661 | 2661 | | |
2662 | 2662 | | |
2663 | | - | |
| 2663 | + | |
2664 | 2664 | | |
2665 | | - | |
| 2665 | + | |
| 2666 | + | |
2666 | 2667 | | |
2667 | 2668 | | |
2668 | 2669 | | |
2669 | 2670 | | |
2670 | | - | |
| 2671 | + | |
2671 | 2672 | | |
2672 | 2673 | | |
2673 | 2674 | | |
| |||
2683 | 2684 | | |
2684 | 2685 | | |
2685 | 2686 | | |
2686 | | - | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2660 | 2660 | | |
2661 | 2661 | | |
2662 | 2662 | | |
2663 | | - | |
| 2663 | + | |
2664 | 2664 | | |
2665 | | - | |
| 2665 | + | |
| 2666 | + | |
2666 | 2667 | | |
2667 | 2668 | | |
2668 | 2669 | | |
2669 | 2670 | | |
2670 | | - | |
| 2671 | + | |
2671 | 2672 | | |
2672 | 2673 | | |
2673 | 2674 | | |
| |||
2683 | 2684 | | |
2684 | 2685 | | |
2685 | 2686 | | |
2686 | | - | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2660 | 2660 | | |
2661 | 2661 | | |
2662 | 2662 | | |
2663 | | - | |
| 2663 | + | |
2664 | 2664 | | |
2665 | | - | |
| 2665 | + | |
| 2666 | + | |
2666 | 2667 | | |
2667 | 2668 | | |
2668 | 2669 | | |
2669 | 2670 | | |
2670 | | - | |
| 2671 | + | |
2671 | 2672 | | |
2672 | 2673 | | |
2673 | 2674 | | |
| |||
2683 | 2684 | | |
2684 | 2685 | | |
2685 | 2686 | | |
2686 | | - | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2660 | 2660 | | |
2661 | 2661 | | |
2662 | 2662 | | |
2663 | | - | |
| 2663 | + | |
2664 | 2664 | | |
2665 | | - | |
| 2665 | + | |
| 2666 | + | |
2666 | 2667 | | |
2667 | 2668 | | |
2668 | 2669 | | |
2669 | 2670 | | |
2670 | | - | |
| 2671 | + | |
2671 | 2672 | | |
2672 | 2673 | | |
2673 | 2674 | | |
| |||
2683 | 2684 | | |
2684 | 2685 | | |
2685 | 2686 | | |
2686 | | - | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2660 | 2660 | | |
2661 | 2661 | | |
2662 | 2662 | | |
2663 | | - | |
| 2663 | + | |
2664 | 2664 | | |
2665 | | - | |
| 2665 | + | |
| 2666 | + | |
2666 | 2667 | | |
2667 | 2668 | | |
2668 | 2669 | | |
2669 | 2670 | | |
2670 | | - | |
| 2671 | + | |
2671 | 2672 | | |
2672 | 2673 | | |
2673 | 2674 | | |
| |||
2683 | 2684 | | |
2684 | 2685 | | |
2685 | 2686 | | |
2686 | | - | |
| 2687 | + | |
2687 | 2688 | | |
2688 | 2689 | | |
2689 | 2690 | | |
| |||
0 commit comments