Repository navigation
Commit a794e32
feat(platform-objects): mint and revoke SCIM connection credentials from the console (#31119)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi9Hju7DThD7eoLKoJU9hnHj)_
Fixes #31032
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么:** `sys_scim_connection_credential` 加了两个操作:「签发凭据」(列表工具栏)调用已有的
`POST
/api/v1/auth/admin/scim/mint-credential`,令牌在结果对话框里只显示一次;「撤销凭据」(凭据行和记录页头)调用
`POST /api/v1/auth/admin/scim/revoke-credential`,先确认再执行。四种语言的翻译一并补上。可选第
4 项也做了:在模拟登录会话里签发时,审计行多记一个 `impersonated_by`。
- **为什么改:** 这两个路由已经上线,但控制台里够不着,平台管理员配置 SCIM 预配时仍要用 curl 拿 Bearer 令牌。
- **风险与回滚:** 不放宽谁能签发或撤销:两个操作只对平台管理员显示,声明的权限就是路由自己的门槛,最终仍由路由裁决。签发不暴露给 AI
工具(ADR-0011 H 档),撤销暴露但必须人工确认(C 档)。回滚即 revert。
- **先看:** ①
`packages/platform-objects/src/identity/sys-scim-connection-credential.object.ts`(两个操作和
AI 分档);②
`packages/plugins/plugin-auth/src/scim-connection-admin.ts`(审计行的
`impersonated_by`);③ `sys-scim-connection-credential-actions.test.ts`。
- **两分钟验证:** `pnpm --filter @objectstack/platform-objects exec vitest
run src/identity/sys-scim-connection-credential-actions.test.ts
src/identity/two-factor-one-shot-reveal.test.ts`,应为 12 个用例全绿。
- **需要你做的:** 无,席位落地。
## What changes
- **`sys_scim_connection_credential` gains `mint_scim_credential`**
(list toolbar, `type: 'api'`, `POST
/api/v1/auth/admin/scim/mint-credential`). Its params are named for the
body keys the mount reads: `connectionId` (required), `label`,
`organizationId` (a `sys_organization` lookup), `provisioningDomainId`,
`expiresAt`. The mint answers the plaintext bearer once (`Cache-Control:
no-store`; only the keyed digest is stored), so it is revealed in a
`resultDialog` (`token` as `secret`, plus `connectionId` and
`expiresAt`) the admin must close. The mount is keyed by the connection
id, and the credential row is what it writes, so the action sits on the
credential object, not the binding object (the binding is the library's
lifecycle row and the mount never reads it).
- **`revoke_scim_credential`** (list item and record header, `POST
/api/v1/auth/admin/scim/revoke-credential`, `recordIdParam:
'credentialId'`, a confirmation, no param dialog). Offered on an active
credential only (`has(record.active) && record.active == true`). The
mount's 404 and 409 envelopes reach the admin through the console's
api-action refusal path (`apiActionRefusalResult`), as every other
identity action's do.
- **Who is offered them.** Both carry `visible:
'current_user.isPlatformAdmin == true'` and `requiredPermissions:
[...PLATFORM_ADMIN_SETTINGS_GATE]`, the declarations every action on an
`/api/v1/auth/admin/` door carries. They narrow who is offered the
button to who the door already admits; the door's platform-admin gate is
unchanged, and no second permission path is added.
- **AI exposure (ADR-0011 identity-actions note).** Mint is bucket H: it
mints a credential, so it declares no `ai` block (an agent that ran it
would hold a live SCIM bearer). Revoke is bucket C: `ai: { exposed:
true, description, requiresConfirmation: true }`. It hands out no
credential and widens no access; it switches one bearer off, as
`sys_api_key.revoke_api_key` and `sys_session.revoke_session` (both C)
do. It cannot be undone, so the confirmation is required.
- **Precedent for the one-shot reveal:** `sys_api_key.create_api_key`
and `sys_oauth_application.create_oauth_application` (a `resultDialog`
with a `format: 'secret'` field and no `successMessage`).
- **Translations** via `node scripts/check-i18n-bundles.mjs --write`;
the zh-CN, zh-Hant, ja-JP and es-ES values written by hand (no en echo
left, so no provenance rows added).
- **Item 4 (`plugin-auth`).** A mint made on an impersonation session
writes the administrator at the keyboard into its `sys_audit_log` row as
`metadata.impersonated_by`. The key is the one the audit writers already
use (`IMPERSONATED_BY_KEY` in plugin-audit), not the card's
`impersonatedBy` spelling. The gate reads it off the session, never the
body. The revoke door's row is unchanged (the order scoped item 4 to the
mint door).
## Tests
- New `src/identity/sys-scim-connection-credential-actions.test.ts` (6
cases): mint's route, method, location and body-key params; revoke's
route, method, `recordIdParam`, no params; revoke's `visible` on active
/ revoked / absent / null `active`; a live control that both mounts have
one action each; mint not AI-exposed; revoke exposed with
`requiresConfirmation`.
- `two-factor-one-shot-reveal.test.ts`: the mint route joins
`ONE_SHOT_ROUTES` with its secret key `token`, so the class rule (a
`resultDialog` covering the secret, no `successMessage`) holds this
action and any later one on the route.
- `platform-admin-affordance-standing.test.ts`: both actions join the
family floor (the family is derived by target; the floor makes the
instrument's reach explicit).
- `plugin-auth` `scim-connection-admin.test.ts`: a mint on an
impersonation session (with a forged `impersonatedBy` in the body)
writes `impersonated_by` from the session. The existing first case pins
the exact metadata of an ordinary mint, so it is the absent half.
## Acceptance notes
- **No Setup navigation entry reaches this object.** Neither
`SETUP_NAV_CONTRIBUTIONS` nor plugin-auth's contributions name
`sys_scim_connection_credential` (the SSO entry is contributed by
plugin-auth only while SSO is wired). The actions are reachable once the
object's list is opened, but a platform admin has no menu item for it. A
gated entry belongs in plugin-auth's `navigationContributions` beside
the SSO one (there is no `scim` public auth feature to gate a static
entry on). Outside this card's file surface; named for the seat.
- **ADR-0011's addendum table** lists the identity actions by bucket and
is read by reviewers, with no automatic check. These two actions are not
in it yet (mint H, revoke C). Editing it is a Tier H ADR change, outside
this card's file surface; named for the seat.
- The two new pins on AI exposure are per-action pins the order asked
for, scoped to these two actions; they are not the table-wide automatic
check the ADR-0011 addendum records as struck.
- Not measured: a live click through the console. The data half of the
reveal (`token` at the inner `data` path) is read from the mount's
response shape, which `scim-connection-admin.test.ts` pins.
## Verification
All readings at head `82b61834`, branched from and merged with
`origin/main` `5f0037449` (main has not moved since).
- `pnpm --filter @objectstack/platform-objects test`: exit 0, 118 files,
1644 passed.
- `pnpm --filter @objectstack/plugin-auth test`: exit 0, 232 files, 4190
passed, 10 skipped.
- `pnpm --filter @objectstack/platform-objects --filter
@objectstack/plugin-auth typecheck` (incl. `check:test-typecheck`): exit
0. The first run caught one TS18048 in the new test
(`resultDialog.fields` possibly undefined), fixed in `82b61834`.
- eslint `--no-inline-config` over `packages/platform-objects` and
`packages/plugins/plugin-auth`: exit 0.
- `node scripts/check-i18n-bundles.mjs --write` re-run after the hand
translations: no diff; `pnpm check:i18n`: exit 0.
- `dispatch-gates --commands` derived 63 families, all run with exit
codes recorded; `--ran`: 63 derived, 63 run, 0 NOT-MEASURED (a derived
zero). Two exited 3 on the first pass for missing prerequisites and were
re-run green after supplying them: `check-plugin-teardown-shape
--self-test` (fetched its pinned fixture commit) and
`check:dual-build-cjs-loads` (after `check:type-check-debt` built the
packages). The derived list is identical at the final head.
<details><summary>Ablations (each through scripts/ablation-replace.mjs:
anchor hit, mutation landed, run red, restored with blob ==
HEAD)</summary>
- A1, mint declares `ai: { exposed: true, description }`: `mint is NOT
exposed to AI tools` fails, "mint_scim_credential declares ai.exposed:
expected true to be false" (1 failed, 5 passed).
- A2, revoke `visible` without the `active` guard: `revoke is offered on
an active credential, not on a revoked one` fails, "expected 'true' to
be 'false'".
- A3, the secret field's path `token` changed to `credentialId`: the
one-shot reveal class pin fails ("does not reveal 'token'") and the new
mint pin fails.
- A4, revoke without `requiresConfirmation`: `revoke is exposed with its
confirmation` fails.
- A5, plugin-auth writes `impersonated_by` on every mint
(`actor.impersonatedBy ?? actor.id`): the ordinary-mint case fails on
its exact metadata (3 keys vs 2).
The platform-objects pins import the object sources directly, so no
`dist/` rebuild was involved; the plugin-auth suite imports `src` the
same way.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on both touched packages 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: not touched.
- [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 (the 维护者速读 block is the
repo's own Chinese reader section).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
Session: https://claude.ai/code/session_017ihL2c3bRFyY1fQ6vWAW1Z
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 98583cd commit a794e32
12 files changed
Lines changed: 504 additions & 2 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2282 | 2282 | | |
2283 | 2283 | | |
2284 | 2284 | | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
| 2315 | + | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| 2321 | + | |
| 2322 | + | |
| 2323 | + | |
| 2324 | + | |
| 2325 | + | |
| 2326 | + | |
| 2327 | + | |
2285 | 2328 | | |
2286 | 2329 | | |
2287 | 2330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2282 | 2282 | | |
2283 | 2283 | | |
2284 | 2284 | | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
| 2315 | + | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| 2321 | + | |
| 2322 | + | |
| 2323 | + | |
| 2324 | + | |
| 2325 | + | |
| 2326 | + | |
| 2327 | + | |
2285 | 2328 | | |
2286 | 2329 | | |
2287 | 2330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2282 | 2282 | | |
2283 | 2283 | | |
2284 | 2284 | | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
| 2315 | + | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| 2321 | + | |
| 2322 | + | |
| 2323 | + | |
| 2324 | + | |
| 2325 | + | |
| 2326 | + | |
| 2327 | + | |
2285 | 2328 | | |
2286 | 2329 | | |
2287 | 2330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2282 | 2282 | | |
2283 | 2283 | | |
2284 | 2284 | | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
| 2315 | + | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| 2321 | + | |
| 2322 | + | |
| 2323 | + | |
| 2324 | + | |
| 2325 | + | |
| 2326 | + | |
| 2327 | + | |
2285 | 2328 | | |
2286 | 2329 | | |
2287 | 2330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2282 | 2282 | | |
2283 | 2283 | | |
2284 | 2284 | | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
| 2315 | + | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| 2321 | + | |
| 2322 | + | |
| 2323 | + | |
| 2324 | + | |
| 2325 | + | |
| 2326 | + | |
| 2327 | + | |
2285 | 2328 | | |
2286 | 2329 | | |
2287 | 2330 | | |
| |||
| 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 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
0 commit comments