Skip to content

Commit ed4b832

Browse files
feat(plugin-auth): a SCIM group push grants the ordinary permission set its group mapping names (#31443)
Refs #30959 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:身份提供商经 SCIM 推送的组,现在可以按平台管理员保存的映射,给组成员授予一个普通权限集;成员移出组,授权随之撤销。 - 为什么改:此前 IdP 推送的组只落库、不授权,客户必须把每个组成员关系再手工授一遍权限集(卡 #30959,裁决「以上都同意」)。 - 风险与回滚:映射永远不能授予管理员身份,保存时用 plugin-security 自己的组织边界判定拒绝;手工授予的权限从不被对账删除。回滚即撤掉本 PR,没有映射时行为与今天一致。 - 先看:`packages/plugins/plugin-auth/src/scim-group-projection.ts`(对账只删带本投影标记的授权),`packages/plugins/plugin-auth/src/idp-group-mapping-admin.ts`(保存时的边界拒绝),`packages/plugins/plugin-security/src/delegated-admin-gate.ts`(判定函数原样上提)。 - 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run src/scim-group-projection.test.ts`,预期 14 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes An identity provider that pushes a group over SCIM now changes what its members may do, through a mapping a platform administrator saves. - **Storage.** New platform object `sys_idp_group_mapping` (`packages/platform-objects/src/identity/sys-idp-group-mapping.object.ts`): one row per mapping with typed columns `source_kind` (closed select, V1 value `scim`, the seam #30967 reuses for SSO group claims), `source_id` (the SCIM connection), `group_external_id` or `group_display_name` (exactly one), `permission_set` and `organization_id`. The data API only lists it. One row in `PLATFORM_OBJECTS_BY_PACKAGE`. - **Writer.** `POST /api/v1/auth/admin/scim/set-group-mapping` (`connectionId`, `groupExternalId` or `groupDisplayName`, `permissionSet`) and `POST /api/v1/auth/admin/scim/delete-group-mapping` (`mappingId`), behind the same platform-admin gate as `/admin/scim/mint-credential`. Both are in the route ledger, its conformance pin and the two dogfood admin-route censuses. The organization is the one that owns the connection's live credentials (active and unexpired, the same check the SCIM bearer verifier uses); a connection with no credential is refused `reference_not_found`, one whose every credential is revoked or expired, or whose live credentials name no organization or two, `rule_violation`. Re-saving the same group and set is idempotent in that organization; when the same-key row was saved while another organization owned the connection, the save is refused `409 RESOURCE_CONFLICT` naming that mapping (delete it first), so the answer never names an organization the row does not carry. - **Admin boundary.** The save refuses every set outside the organization boundary with `400 VALIDATION_FAILED`, field `permissionSet`, code `rule_violation`, and stores nothing: `admin_full_access`, the organization-administrator sets, the agent ceiling sets, a set with a superuser wildcard or an `adminScope`, and a set with a capability not scoped to the organization. The judgement is plugin-security's own `organizationBoundaryFailure`, lifted verbatim from a private method of `DelegatedAdminGate` to a module function (the method is now a one-line delegate) and served as an optional member of the registered `security` service. plugin-auth takes no dependency on plugin-security and keeps no copy of the list; it feature-detects the member and fails closed (`503`, nothing saved, nothing granted). - **Spec.** One optional member, `ISecurityService.organizationBoundaryFailure?(setName: string)`, resolving to the failure reason as a string or `null` when the set is inside the boundary (`packages/spec/src/contracts/security-service.ts`), declared in `registered-security-service-members.pin.test.ts`. Optional, so every existing implementer still type-checks. - **Mechanism.** `scim({ … })` in `auth-manager-plugin-list.ts` now takes the vendor `projection` (`createScimGroupProjection`). `roles.map` returns the connection's mapping rows that match the group (by external id when the row names one, else by exact display name); `roles.exists` re-judges the boundary at grant time; `reconcileUser` writes through the ObjectQL engine inside the SCIM transaction with the request's system context, so the audit actor is `svc:scim:CONNECTION_ID`. An engine refusal becomes the vendor's `APIError`, never a SCIM 500. - **Provenance.** A projected grant carries `reason` = `scim-group-projection: provisioning domain 'ID'`. Reconcile deletes only rows with that exact marker, so a grant made by hand, including one for the same set, is never removed, and a set already held by hand is not written again. Saving or deleting a mapping re-runs the vendor's `reconcileSCIMProjection` for the connection, so the change applies to current members at once. - **Last-admin guard.** Its texts no longer tell operators that "the SCIM group mapping is too broad": a mapping never moves administrator standing, so the refusal now says to grant another account administrator standing first. - **Translations.** The new object's labels are translated in every translated locale `main` ships (now nine, with pt-BR, ru and de); the bundles are regenerated, and `bundle-ownership.test.ts` lists the object. - **Ownership ceremony.** `@objectstack/cli`'s ADR-0131 D10 inventory (`organization-ownership-inventory.ts`) gains a row for `sys_idp_group_mapping` with the attribution fate, like `sys_connector_connection` beside it: every mapping row is written with its organization, so a NULL row has no producer. Without it `os migrate organization-ownership` refused any database holding the new table. - **Censuses.** The tenant-audit census (`content/docs/permissions/tenant-audit-census.mdx` and its counts file) and the platform-object tenancy census are regenerated for the three new write sites and the new object; the page's hand-written figures are updated to the census. **Follow-up:** the ADR recording this decision (mechanism, storage, admin boundary, vendor `@better-auth/scim` 1.7.12) plus one pointer line in ADR-0134 D4 comes as a second PR from this branch after this one merges, so its symbol anchors resolve against landed code. That PR says `Fixes #30959`, so the card stays open until the ADR lands (this build PR only refers to it). ## Tests `packages/plugins/plugin-auth/src/scim-group-projection.test.ts` drives the real producer: `AuthManager.handleRequest()` with a real SCIM bearer over `@better-auth/scim` 1.7.12, on a real ObjectQL engine over better-sqlite3, with mappings saved through the door's own function and judged by the real plugin-security `security` service over a test catalog. | Row | Pin | Ablation (mutation → red) | |---|---|---| | (1) mapped group push grants the set, actor `svc:scim:okta-30959` | grant row + one insert with that actor | projection wiring removed → red; writer without the SCIM context → red | | (2) member removed → grant revoked | row gone + one delete with that actor | projection wiring removed → red; writer without the SCIM context → red | | (3) admin set refused at save | 400, `VALIDATION_FAILED`, `[permissionSet, rule_violation]`, no row | boundary refusal disabled → red | | (4) set outside the organization boundary refused at save | same shape | boundary refusal disabled → red | | (5) provisioning with no mapping still works | SCIM create + PATCH 2xx, no grant | regression pin | | CONTROL unmapped group | no grant | group match removed from `roles.map` → red | | CONTROL hand-made grants (`admin_full_access` + the mapped set by hand) | untouched by push and removal | provenance marker check removed → red | | save/delete reproject current members | grant appears on save, goes on delete | projection wiring removed → red | | (S1) connection whose credential names no organization refused at save | 400, `VALIDATION_FAILED`, `[connectionId, rule_violation]`, no row | door's no-organization check disabled → red | | (S1) projection never writes a grant without an organization | org-less mapping read: no grant, one warning | reconcile's `admits` call removed → red; `admits` organization check disabled → red | | (S2) reconcile judges the boundary itself: set redefined as administrator after the save | `reconcileUser` called without `roles.exists`: no grant, one warning; a real push then grants nothing | reconcile's `admits` call removed → red | | (S4) credential revoked in organization A, a new one minted in B | save 200 into B, the row carries B | live-credential filter removed from the door → red | | (S4) every credential revoked | 400, `VALIDATION_FAILED`, `[connectionId, rule_violation]`, no row | live-credential filter removed from the door → red | | (S5) re-save after revoke in A and mint in B | 409, `RESOURCE_CONFLICT`, message names the A mapping and A, the single row stays in A; CONTROL same-organization re-save answers 200 `created: false` | organization match removed from the door → red | Each ablation ran through `scripts/ablation-replace.mjs` (anchor hit 1 → 0, blob restored equal to HEAD). The three S1/S2 pins were committed red first (`cdd23489`) before the fix (`fe3aea79`), the two S4 pins likewise (`ef76a598` before `ff3c8f8e`), and the S5 pin too (`82e0cee3` before `18a95de4`). `scim-connection-service.test.ts` adds unit cases for `scimCredentialIsLive` (active false, 0, null or absent; no expiry; past; exactly now; future; unreadable). ## Acceptance notes - Matching reading: each mapping row names exactly one group key, and matching is per row (external id when the row has one, else exact display name). - Beyond the acceptance rows: saving or deleting a mapping reprojects current members at once, and the boundary is re-judged at grant time, so a set later redefined with admin bits grants nothing (warned once). - Known edge: two provisioning domains that project the same (set, organization) share one grant row; the marker names the domain that wrote it. - No Console surface for mappings in this PR; the two doors are the only writer. - Serial with #30967: whichever lands second merges `origin/main` once and re-runs the plugin-auth and platform-objects suites. ## Verification Head `d8ef8e69`, after merging `origin/main` four times without rebasing (`1efd345f`, `ae12be81`, `52aa0fc6`, and `1ccf149e` at `645ed3d1`, which includes the de bundle from #31511). The first merge resolved `delegated-admin-gate.ts` by keeping #31384's helpers; the lifted predicate is still verbatim. The census artefacts were regenerated from main's copy. - Build: `turbo run build` 120/120 tasks. - Typecheck: `pnpm --filter PKG typecheck` (including each package's `check:test-typecheck`) exit 0 for plugin-security, platform-objects, plugin-auth, spec and cli; `check:type-check-debt` exit 0. - Lint: `eslint --no-inline-config` over the touched packages, exit 0. - Suites on the merged tree: plugin-auth 243 files, 4337 passed / 10 skipped (on `d8ef8e69`); plugin-security 5886 passed / 45 skipped; platform-objects 1886 passed (on `d8ef8e69`); cli 7582 passed / 9 skipped (unit and integration). The ownership-ceremony files (inventory unit, `dev-organization-ownership-stale-column`, `start-organization-ownership-refusal`, `declared-rule-mirrors`, `no-organization`, `sqlite-wasm`) were 10 red before the inventory row and are 42/42 green on `42f60764` with cli rebuilt. - i18n: `check:i18n`, `check:i18n-coverage` (none new), `check:i18n-stale-fill` and `check:i18n-walk-parity` exit 0 on `d8ef8e69`; the de bundle keeps only the `id` source-hash entry, like the other locales. `check-tenant-audit-census`, `check:platform-object-tenancy-census` and `check:auth-mount-ledger` exit 0 there too. - Acceptance file: 14/14 passed; ten ablations red as listed under Tests. - Derived gates: `dispatch-gates --commands` derived 130 families on `95349ede`; all 130 ran there, every one exit 0, and `--ran` reconciled 130 run, 0 NOT-MEASURED, 0 UNRUN. `d8ef8e69` adds only the main merge and the de bundle; the i18n, census and ledger gates above were re-run on it. `pnpm --filter @objectstack/plugin-auth typecheck` (with `check:test-typecheck`) exit 0 on `95349ede`. - Line budget against `main`: 48 files, +2061 −111. Generated output (translation bundles in nine locales, two census artefacts and the tenancy census JSON) is 585 of it; authored is 26 files, +1476 −69. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5mEnsnzoPLq43wQyYy)_ <!-- signed: domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 133f75a commit ed4b832

48 files changed

Lines changed: 2061 additions & 111 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/plugin-security': patch
4+
'@objectstack/platform-objects': patch
5+
'@objectstack/spec': patch
6+
'@objectstack/cli': patch
7+
---
8+
9+
feat(plugin-auth): a SCIM group push grants the ordinary permission set its group mapping names
10+
11+
Clause-②: yes (widening)
12+
Metadata-protocol: none
13+
14+
An identity provider that pushes a group over SCIM now changes what its members may do. A platform administrator saves a mapping from one SCIM connection's group to one permission set with `POST /api/v1/auth/admin/scim/set-group-mapping` (`connectionId`, `groupExternalId` or `groupDisplayName`, `permissionSet`), and deletes one with `POST /api/v1/auth/admin/scim/delete-group-mapping` (`mappingId`). Both run the platform-admin gate the SCIM credential door runs. A connection whose credential names no organization, or whose credentials name two, is refused `400 VALIDATION_FAILED` on `connectionId`, so every mapping, and every grant it makes, belongs to exactly one organization. Only live credentials (active and unexpired, as the bearer verifier reads them) decide that organization: a credential revoked in one organization does not block a mapping once a new one is minted in another. Saving the same group and set again is idempotent; if that mapping was saved while another organization owned the connection, the save is refused `409 RESOURCE_CONFLICT` naming the old mapping, which must be deleted first. The rows live in the new platform object `sys_idp_group_mapping`, which the data API only lists.
15+
16+
When a group's membership changes, `@better-auth/scim`'s projection grants each member the mapped set in the organization that owns the connection, inside the SCIM transaction, written by the connection (`svc:scim:CONNECTION_ID` in the audit trail). Removing a member removes that grant. A group nobody mapped grants nothing. The projection asks the boundary again before every grant it writes, so a set that becomes an administrator set after its mapping was saved grants nothing. A grant the projection did not make, such as one an administrator made by hand, is never removed. Saving or deleting a mapping applies it to the group's current members at once.
17+
18+
A mapping can never make anyone an administrator. The save refuses, with `400 VALIDATION_FAILED` on `permissionSet`, every set outside the organization boundary: `admin_full_access`, the organization-administrator sets, the agent ceiling sets, a set with a superuser wildcard or an `adminScope`, and a set with a capability not scoped to the organization. The judgement is plugin-security's own delegated-administration predicate, now served as the optional `ISecurityService.organizationBoundaryFailure(setName)` member. Without a security service the save is refused `503` and nothing is granted.
19+
20+
The break-glass guard's refusal no longer tells operators that "the SCIM group mapping is too broad": a group mapping never moves administrator standing, so the sentence now says to grant another account standing first.
21+
22+
The v18 organization-ownership ceremony (`os migrate organization-ownership`) inventories `sys_idp_group_mapping` with the attribution fate, like the other organization-owned rows written with their organization: a database that holds the new table plans and applies as before.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ are reported as `undecidable` rather than assumed either way.
132132

133133
The same holds twice over for the context. An options argument spelled as a
134134
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
135-
forwarding shim cannot, and **56 of the 270 sites are spelled that way**. A
135+
forwarding shim cannot, and **57 of the 275 sites are spelled that way**. A
136136
context resolved from an inline literal or a local `const` can be tested for
137137
`isSystem`; one arriving from a helper call cannot.
138138

@@ -160,8 +160,8 @@ now **0**: nothing on this surface threads a context that provably lacks the fla
160160

161161
**"No tenant context" counted sites it had not read.** An options argument the
162162
walker could not parse was folded into the same bucket as one it had read and
163-
found empty. That published **64 sites "carrying no tenant context at all"**
164-
when 8 said so and 56 were simply unread — an over-claim in the *alarming*
163+
found empty. That published **67 sites "carrying no tenant context at all"**
164+
when 10 said so and 57 were simply unread — an over-claim in the *alarming*
165165
direction, on the very figure this page tells other cards to cite. `carries` is
166166
now three-valued, and an unreadable argument can never contribute to the
167167
provable count.
@@ -197,10 +197,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
197197

198198
| carried figure | where it survives | this census |
199199
| :--- | :--- | ---: |
200-
| 175 write call sites | quoted in the merged changeset | **270** |
201-
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **30** more whose options argument is unreadable |
202-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **192 of 270** decidable, **78** undecidable |
203-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 150 decidably elevated, 0 decidably not, 112 undecidable |
200+
| 175 write call sites | quoted in the merged changeset | **275** |
201+
| 24 carrying no tenant context | quoted in the merged changeset | **3** provable and tenancy-enabled; **30** more whose options argument is unreadable |
202+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **195 of 275** decidable, **80** undecidable |
203+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 152 decidably elevated, 0 decidably not, 113 undecidable |
204204
| 141 and 132, two independent re-derivations | the card that filed this work | — |
205205

206206
**The differences are not reconciled, and deliberately so.** The old census's
@@ -211,17 +211,17 @@ at any commit.
211211

212212
Two structural facts do plausibly widen this reading against any hand or regex
213213
one, and both are counted in the generated tables below: the 62 sites reached
214-
through an erased (`any`) receiver, and the 69 that name their object through a
214+
through an erased (`any`) receiver, and the 71 that name their object through a
215215
`const` rather than inline. An instrument that read either the way a person does
216216
would report a smaller number and would not say so.
217217

218218
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
219219
figure has no surviving corroboration anywhere in the tree.** This census reads
220-
150 of 270 (56%) as decidably elevated, with 112 more whose elevation is a
220+
152 of 275 (55%) as decidably elevated, with 113 more whose elevation is a
221221
run-time fact — so the claim is neither confirmed nor refuted, and the honest
222222
answer is that a static reading cannot settle it.
223223

224-
⇒ **Cite `2 / 270`, and say what it is**: the sites whose options argument was
224+
⇒ **Cite `3 / 275`, and say what it is**: the sites whose options argument was
225225
READ and holds no tenant context, against a decidably tenancy-enabled object.
226226
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
227227
without tenant context" — **30 further sites** have an options argument this
@@ -233,31 +233,31 @@ cannot read, and they are neither in nor out.
233233

234234
| what | count |
235235
| :--- | ---: |
236-
| write call sites on the application surface | **270** |
237-
| …whose object name is statically decidable | 192 |
238-
| …whose object name is chosen at run time | 78 |
239-
| …against an object with tenancy ENABLED | 186 |
236+
| write call sites on the application surface | **275** |
237+
| …whose object name is statically decidable | 195 |
238+
| …whose object name is chosen at run time | 80 |
239+
| …against an object with tenancy ENABLED | 189 |
240240
| …against an object that declares tenancy off | 6 |
241-
| threading a tenant context | 206 |
242-
| PROVABLY carrying none (options read, no context key) | **8** |
243-
| …of those, against a decidably tenancy-enabled object | **2** |
244-
| options argument UNREADABLE — may or may not carry one | 56 |
241+
| threading a tenant context | 208 |
242+
| PROVABLY carrying none (options read, no context key) | **10** |
243+
| …of those, against a decidably tenancy-enabled object | **3** |
244+
| options argument UNREADABLE — may or may not carry one | 57 |
245245
| …of those, against a decidably tenancy-enabled object | 30 |
246-
| threading a decidably ELEVATED (`isSystem`) context | 150 |
246+
| threading a decidably ELEVATED (`isSystem`) context | 152 |
247247
| threading a context that is decidably NOT elevated | 0 |
248-
| threading a context whose elevation is a run-time fact | 112 |
248+
| threading a context whose elevation is a run-time fact | 113 |
249249

250250
| how the instrument reached the site | count |
251251
| :--- | ---: |
252-
| receiver carried a readable engine type | 208 |
252+
| receiver carried a readable engine type | 213 |
253253
| receiver erased, placed by the object NAME | 52 |
254254
| receiver erased, placed by an `object: string` PARAMETER | 5 |
255255
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
256256

257-
| object name spelled inline | 123 |
258-
| object name spelled through a `const` | 69 |
257+
| object name spelled inline | 124 |
258+
| object name spelled through a `const` | 71 |
259259
| object name is an `object: string` parameter | 7 |
260-
| object name is some other run-time expression | 71 |
260+
| object name is some other run-time expression | 73 |
261261

262262
### Subtractions the census could NOT defend — enforced
263263

@@ -307,13 +307,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
307307
their values are not compared. The reasoning, and the measurement behind it,
308308
are in `scripts/check-tenant-audit-census.mjs`.
309309

310-
Measured on 2026-11-08 at `759f4daa0`.
310+
Measured on 2026-11-08 at `fe3aea79c`.
311311

312312
| corpus scale (not enforced) | count |
313313
| :--- | ---: |
314-
| tracked non-test sources scanned | 1060 |
314+
| tracked non-test sources scanned | 1063 |
315315
| engine-shaped types recognised | 114 |
316-
| declared objects in the registry | 131 |
316+
| declared objects in the registry | 132 |
317317
| same-named calls subtracted as non-engine | 243 |
318318

319319
{/* END GENERATED: tenant-audit-census */}

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3333

3434
| Measure | Value |
3535
|---|---:|
36-
| Write call sites | 270 |
37-
| Object name statically decidable | 192 |
38-
| Object name chosen at run time | 78 |
39-
| Against a tenancy-enabled object | 186 |
36+
| Write call sites | 275 |
37+
| Object name statically decidable | 195 |
38+
| Object name chosen at run time | 80 |
39+
| Against a tenancy-enabled object | 189 |
4040
| Against an object declaring tenancy off | 6 |
41-
| Threading a tenant context | 206 |
42-
| Provably carrying none | 8 |
43-
| …and decidably tenancy-enabled | 2 |
44-
| Options argument unreadable | 56 |
41+
| Threading a tenant context | 208 |
42+
| Provably carrying none | 10 |
43+
| …and decidably tenancy-enabled | 3 |
44+
| Options argument unreadable | 57 |
4545
| …and decidably tenancy-enabled | 30 |
46-
| Threading a decidably elevated context | 150 |
46+
| Threading a decidably elevated context | 152 |
4747
| Threading a decidably non-elevated context | 0 |
48-
| Threading a context of undecidable elevation | 112 |
48+
| Threading a context of undecidable elevation | 113 |
4949

5050
## Subtractions the census could NOT defend — enforced
5151

@@ -90,13 +90,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-11-08 at `759f4daa0`.
93+
Measured on 2026-11-08 at `fe3aea79c`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 1060 |
97+
| tracked non-test sources scanned | 1063 |
9898
| engine-shaped types recognised | 114 |
99-
| declared objects in the registry | 131 |
99+
| declared objects in the registry | 132 |
100100
| same-named calls subtracted as non-engine | 243 |
101101

102102
## Every site
@@ -155,6 +155,9 @@ Measured on 2026-11-08 at `759f4daa0`.
155155
| `packages/plugins/plugin-auth/src/ensure-default-organization.ts` | `update` | `sys_member` | enabled | elevated | 1 |
156156
| `packages/plugins/plugin-auth/src/forced-reset-revocation.ts` | `update` | `SystemObjectName.API_KEY` | undecidable | elevated | 1 |
157157
| `packages/plugins/plugin-auth/src/forced-reset-revocation.ts` | `update` | `SystemObjectName.SESSION` | undecidable | elevated | 1 |
158+
| `packages/plugins/plugin-auth/src/idp-group-mapping-admin.ts` | `delete` | `IDP_GROUP_MAPPING_OBJECT` | undecidable | PROVABLY NONE | 1 |
159+
| `packages/plugins/plugin-auth/src/idp-group-mapping-admin.ts` | `insert` | `IDP_GROUP_MAPPING_OBJECT` | undecidable | options unreadable | 1 |
160+
| `packages/plugins/plugin-auth/src/idp-group-mapping-admin.ts` | `insert` | `sys_audit_log` | enabled | elevated | 1 |
158161
| `packages/plugins/plugin-auth/src/idp-locale-claim.ts` | `update` | `sys_user` | enabled | elevated | 1 |
159162
| `packages/plugins/plugin-auth/src/mcp-client-last-use.ts` | `update` | `sys_oauth_consent` | enabled | elevated | 1 |
160163
| `packages/plugins/plugin-auth/src/member-role-canonical.ts` | `update` | `MEMBER_OBJECT` | undecidable | elevated | 1 |
@@ -173,6 +176,8 @@ Measured on 2026-11-08 at `759f4daa0`.
173176
| `packages/plugins/plugin-auth/src/scim-connection-admin.ts` | `insert` | `sys_audit_log` | enabled | elevated | 1 |
174177
| `packages/plugins/plugin-auth/src/scim-connection-service.ts` | `insert` | `sys_scim_connection_credential` | enabled | PROVABLY NONE | 1 |
175178
| `packages/plugins/plugin-auth/src/scim-connection-service.ts` | `update` | `sys_scim_connection_credential` | enabled | elevated | 1 |
179+
| `packages/plugins/plugin-auth/src/scim-group-projection.ts` | `delete` | `sys_user_permission_set` | enabled | PROVABLY NONE | 1 |
180+
| `packages/plugins/plugin-auth/src/scim-group-projection.ts` | `insert` | `sys_user_permission_set` | enabled | elevated | 1 |
176181
| `packages/plugins/plugin-auth/src/session-tombstone.ts` | `update` | `objectName` | undecidable | options unreadable | 1 |
177182
| `packages/plugins/plugin-auth/src/sso-client-secret.ts` | `update` | `sys_sso_provider` | disabled | elevated | 1 |
178183
| `packages/plugins/plugin-auth/src/sys-session-revoke.ts` | `update` | `SystemObjectName.SESSION` | undecidable | elevated | 1 |

‎packages/cli/src/utils/organization-ownership-inventory.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,18 @@ export const ORGANIZATION_OWNERSHIP_INVENTORY: readonly InventoryEntry[] = [
784784
`${CENSUS_MAIN}: systemFields.tenant: false; ${D7}: a declarative connector's stored credential is environment-level, ` +
785785
'read by the reference its instances name, never by organization',
786786
},
787+
// #30959 (ruling record 6718345088, fork 1 (a)) — an identity-provider group
788+
// mapping is saved by the platform administrator for one SCIM connection and
789+
// always carries the organization that owns the connection's credential (the
790+
// save refuses a connection whose credential names none), so a NULL row has
791+
// no producer.
792+
{
793+
object: 'sys_idp_group_mapping',
794+
census: PMAIN,
795+
fate: 'attribution',
796+
citation: `${CENSUS_MAIN} (in reach, added since the first census; one row per SCIM connection, in the organization that owns it); ${D10_SINGLE}`,
797+
anchors: [],
798+
},
787799
// ADR-0143 D3 — a connector connection is made by an organization's
788800
// administrator and dispatched with by that organization's runs (Q1 = A):
789801
// every row is written with the organization that connected it, so a NULL

‎packages/platform-objects/scripts/i18n-extract.config.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ import {
145145
SysScimProjectionGrant,
146146
SysScimSubject,
147147
SysScimUser,
148+
SysIdpGroupMapping,
148149
} from '../src/identity/index.js';
149150

150151
// ── Security ──────────────────────────────────────────────────────────────
@@ -319,6 +320,7 @@ const config: ObjectStackDefinition = defineStack({
319320
SysScimProjectionGrant,
320321
SysScimSubject,
321322
SysScimUser,
323+
SysIdpGroupMapping,
322324

323325
// Security: RBAC moved to @objectstack/plugin-security, sharing to
324326
// @objectstack/plugin-sharing (ADR-0029 K2 / D8).

‎packages/platform-objects/src/apps/translations/ar.objects.generated.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2649,6 +2649,54 @@ export const arObjects: NonNullable<TranslationData['objects']> = {
26492649
}
26502650
}
26512651
},
2652+
sys_idp_group_mapping: {
2653+
label: "ربط مجموعة IdP",
2654+
pluralLabel: "روابط مجموعات IdP",
2655+
description: "يربط مجموعة من مزوّد الهوية بمجموعة الأذونات الممنوحة لأعضائها",
2656+
fields: {
2657+
id: {
2658+
label: "ID"
2659+
},
2660+
source_kind: {
2661+
label: "نوع المصدر",
2662+
help: "ما يرسل المجموعة: اتصال SCIM.",
2663+
options: {
2664+
scim: "اتصال SCIM"
2665+
}
2666+
},
2667+
source_id: {
2668+
label: "معرّف المصدر",
2669+
help: "معرّف اتصال SCIM الذي تصل عبره المجموعة، مثل \"okta-prod\"."
2670+
},
2671+
group_external_id: {
2672+
label: "المعرّف الخارجي للمجموعة",
2673+
help: "قيمة externalId للمجموعة كما يرسلها مزوّد الهوية. اضبط هذا الحقل أو اسم العرض، وليس كليهما."
2674+
},
2675+
group_display_name: {
2676+
label: "اسم عرض المجموعة",
2677+
help: "قيمة displayName للمجموعة، بمطابقة تامة. يُستخدم عندما لا يحدد الربط معرّفًا خارجيًا."
2678+
},
2679+
permission_set: {
2680+
label: "مجموعة الأذونات",
2681+
help: "مجموعة الأذونات الممنوحة لعضو المجموعة. تُرفض مجموعات المسؤول."
2682+
},
2683+
organization_id: {
2684+
label: "المؤسسة",
2685+
help: "المؤسسة التي يتم المنح فيها: المؤسسة المالكة لاتصال SCIM."
2686+
},
2687+
created_at: {
2688+
label: "تاريخ الإنشاء"
2689+
},
2690+
updated_at: {
2691+
label: "تاريخ التحديث"
2692+
}
2693+
},
2694+
_views: {
2695+
all: {
2696+
label: "الكل"
2697+
}
2698+
}
2699+
},
26522700
sys_notification: {
26532701
label: "حدث الإشعار",
26542702
pluralLabel: "أحداث الإشعارات",

‎packages/platform-objects/src/apps/translations/ar.source-hashes.generated.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const arGeneratedSourceHashes: Readonly<Record<string, string>> = {
2525
"metadataForms.permission.fields.tabPermissions.helpText": "8942e2aebe59897c",
2626
"metadataForms.position.fields.name.helpText": "3594bba7cc2a3c8a",
2727
"objects.sys_email.fields.message_id.label": "14cd089a4062f4b1",
28+
"objects.sys_idp_group_mapping.fields.id.label": "00b0385c9c152888",
2829
"objects.sys_job.fields.schedule_type.options.cron": "fd48d0e2cd59cab2",
2930
"objects.sys_oauth_application.fields.jwks.label": "b2cf79f9d66c0dbc",
3031
"objects.sys_oauth_application.fields.jwks_uri.label": "2a4339df734ab269",

‎packages/platform-objects/src/apps/translations/bundle-ownership.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const OWNED_OBJECTS = new Set([
3131
'sys_scim_connection_binding', 'sys_scim_connection_credential', 'sys_scim_group',
3232
'sys_scim_group_member', 'sys_scim_identity_tombstone', 'sys_scim_projection_grant',
3333
'sys_scim_subject', 'sys_scim_user',
34+
// identity — IdP group → permission set mapping (#30959; SCIM now, SSO group claims later)
35+
'sys_idp_group_mapping',
3436
// audit / messaging-adjacent (still owned here)
3537
'sys_notification', 'sys_attachment', 'sys_email', 'sys_email_template',
3638
// (sys_saved_report / sys_report_schedule retired with the saved-report stack, #20102)

0 commit comments

Comments
 (0)