Skip to content

feat(scim): ADR 0024 - Phase 1+2#925

Merged
gtema merged 1 commit into
mainfrom
claude/scim-v2-support-adr-u1f488
Jul 6, 2026
Merged

feat(scim): ADR 0024 - Phase 1+2#925
gtema merged 1 commit into
mainfrom
claude/scim-v2-support-adr-u1f488

Conversation

@gtema

@gtema gtema commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Adds the realm-registration layer for SCIM v2 resource provisioning, the
first of six planned phases. No SCIM Users/Groups endpoints yet - this
phase is pure plumbing to gate future resource CRUD behind an explicit
realm-activation step.

  • New core type ScimRealmResource (+ Create/Update/ListParameters) and
    a ScimRealmProviderError enum, following the ApiClientResource
    pattern.
  • New openstack-keystone-scim-driver-raft crate storing realms at
    scim_realm:v1:<domain_id>:<provider_id>.
  • New ScimRealmApi/ScimRealmService provider
    (ExecutionContext-based, so realm CRUD gets CADF audit trail parity
    with other v4 admin resources) wired through
    PluginManagerApi/Provider/Config alongside the existing
    providers.
  • New /v4/scim-realms CRUD API (create/list/show/update) with OPA
    policies under identity/scim_realm/*, requiring a manager
    (or admin) role.
  • ScimRealmAuth extractor (ADR 0024 §2.C amendment to ADR 0021 §3 Step
    4): wraps the existing API-Key ingress pipeline, additionally
    enforcing Domain-only scope and the Realm Activation Gate (403 if the
    realm is missing/disabled) before any future SCIM resource handler
    runs.
  • Mapping Engine write-time constraint (ADR 0024 §2.C): rejects
    Authorization::Project rules in a ruleset whose provider_id has an
    active SCIM realm, with 422 Unprocessable Entity (new KeystoneApiError
    variant), mirroring the existing is_system defense-in-depth check.

Adds the first fully working SCIM resource: POST/GET/PUT/DELETE
/SCIM/v2/{domain_id}/Users, backed by real Identity users.

  • ScimResourceIndex core type + scim-driver-raft storage, with an atomic
    realm-scoped externalId claim (create_if_absent) closing the TOCTOU
    race for that dimension of ADR 0024 Section 3.D.
  • Domain-wide case-insensitive userName collision check
    (IdentityApi::find_user_by_name_ci) against local_user/nonlocal_user.
  • ScimRealmAuth-gated Users handlers implementing the Ownership Fencing
    Algorithm (Section 3.C), soft-disable delete with token revocation +
    CADF disable event (Section 6.A), and a SCIM-shaped 409 uniqueness
    error envelope.
  • Fixed a latent bug in resolve_verified_api_client: it extracted
    Path for domain_id, which only works when the whole route has
    exactly one dynamic segment; the new /Users/{id} routes have two and
    would have failed every request with 401.
  • OPA policies under identity/scim/user/{create,list,show,update,delete}
    plus their _test.rego pairs.

Assisted-By: Claude Sonnet 5 noreply@anthropic.com
Signed-off-by: Artem Goncharov artem.goncharov@gmail.com

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchclaude/scim-v2-support-adr-u1f488
Testbedubuntu-latest
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
nanoseconds (ns)
(Result Δ%)
Upper Boundary
nanoseconds (ns)
(Limit %)
Command_Serde/apply/remove📈 view plot
🚷 view threshold
128,410.00 ns
(-56.41%)Baseline: 294,601.03 ns
1,683,072.80 ns
(7.63%)
Command_Serde/apply/set📈 view plot
🚷 view threshold
143,970.00 ns
(-43.18%)Baseline: 253,369.39 ns
1,009,153.12 ns
(14.27%)
Command_Serde/pack/delete📈 view plot
🚷 view threshold
128.31 ns
(+5.49%)Baseline: 121.63 ns
146.10 ns
(87.82%)
Command_Serde/pack/delete_index📈 view plot
🚷 view threshold
111.61 ns
(+1.51%)Baseline: 109.95 ns
131.57 ns
(84.83%)
Command_Serde/pack/set📈 view plot
🚷 view threshold
208.39 ns
(+6.56%)Baseline: 195.57 ns
238.99 ns
(87.20%)
Command_Serde/pack/set_index📈 view plot
🚷 view threshold
112.11 ns
(+2.03%)Baseline: 109.88 ns
131.23 ns
(85.43%)
Command_Serde/unpack/delete📈 view plot
🚷 view threshold
195.09 ns
(+1.85%)Baseline: 191.55 ns
235.35 ns
(82.89%)
Command_Serde/unpack/delete_index📈 view plot
🚷 view threshold
163.43 ns
(+2.44%)Baseline: 159.54 ns
199.20 ns
(82.04%)
Command_Serde/unpack/set📈 view plot
🚷 view threshold
290.61 ns
(+7.87%)Baseline: 269.41 ns
333.13 ns
(87.24%)
Command_Serde/unpack/set_index📈 view plot
🚷 view threshold
162.30 ns
(+2.40%)Baseline: 158.50 ns
196.78 ns
(82.48%)
Payload_encryption/pack/remove_cmd📈 view plot
🚷 view threshold
118.28 ns
(+1.89%)Baseline: 116.09 ns
141.81 ns
(83.41%)
Payload_encryption/pack/set_cmd📈 view plot
🚷 view threshold
200.10 ns
(-2.19%)Baseline: 204.58 ns
268.76 ns
(74.45%)
Payload_encryption/unpack/remove_cmd📈 view plot
🚷 view threshold
209.69 ns
(+2.65%)Baseline: 204.27 ns
256.72 ns
(81.68%)
Payload_encryption/unpack/set_cmd📈 view plot
🚷 view threshold
294.82 ns
(+4.67%)Baseline: 281.67 ns
354.16 ns
(83.24%)
Raft_1Node_Latency/prefix/1node📈 view plot
🚷 view threshold
2,614,200.00 ns
(-3.61%)Baseline: 2,712,049.06 ns
6,199,198.72 ns
(42.17%)
Raft_1Node_Latency/read/1node📈 view plot
🚷 view threshold
43,858.00 ns
(+98.33%)Baseline: 22,113.90 ns
70,355.99 ns
(62.34%)
Raft_1Node_Latency/remove/1node📈 view plot
🚷 view threshold
384,860.00 ns
(-33.80%)Baseline: 581,316.25 ns
2,334,726.38 ns
(16.48%)
Raft_1Node_Latency/write/1node📈 view plot
🚷 view threshold
400,890.00 ns
(-33.84%)Baseline: 605,946.88 ns
2,174,528.37 ns
(18.44%)
build_snapshot/default📈 view plot
🚷 view threshold
108,530.00 ns
(-1.13%)Baseline: 109,772.86 ns
145,861.14 ns
(74.41%)
fernet token/project📈 view plot
🚷 view threshold
1,479.30 ns
(+7.03%)Baseline: 1,382.20 ns
1,633.62 ns
(90.55%)
get_data_keyspace📈 view plot
🚷 view threshold
0.31 ns
(-0.12%)Baseline: 0.31 ns
0.36 ns
(85.86%)
get_db📈 view plot
🚷 view threshold
0.31 ns
(-0.11%)Baseline: 0.31 ns
0.36 ns
(85.90%)
get_fernet_token_timestamp/project📈 view plot
🚷 view threshold
133.19 ns
(-7.06%)Baseline: 143.31 ns
181.97 ns
(73.19%)
get_keyspace📈 view plot
🚷 view threshold
4.40 ns
(-11.72%)Baseline: 4.99 ns
10.66 ns
(41.29%)
🐰 View full continuous benchmarking report in Bencher

@gtema gtema force-pushed the claude/scim-v2-support-adr-u1f488 branch from bb3d35d to 3569f8f Compare July 6, 2026 10:36
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦢 Load Test Results

Goose Attack Report

Plan Overview

Action Started Stopped Elapsed Users
Increasing 26-07-06 14:37:04 26-07-06 14:37:19 00:00:15 0 → 30
Maintaining 26-07-06 14:37:19 26-07-06 14:37:49 00:00:30 30
Decreasing 26-07-06 14:37:49 26-07-06 14:38:28 00:00:39 0 ← 30

Request Metrics

Method Name # Requests # Fails Average (ms) Min (ms) Max (ms) RPS Failures/s
DELETE DELETE /v3/auth/tokens 462 0 102.48 8 227 15.40 0.00
DELETE DELETE /v3/projects/:id (teardown) 2 0 44.00 36 52 0.07 0.00
DELETE DELETE /v3/users/:id (teardown) 3 0 57.00 45 76 0.10 0.00
GET 4909 0 95.83 66 182 163.63 0.00
GET GET /v3/auth/tokens (validate new) 461 1 231.54 33 60000 15.37 0.03
GET GET /v3/projects/:id 768 0 77.95 66 115 25.60 0.00
GET GET /v3/projects/:id (catalog) 758 0 78.50 67 108 25.27 0.00
GET GET /v3/users/:id 1071 0 83.91 72 108 35.70 0.00
GET GET /v3/users/:id (catalog) 844 0 82.93 72 104 28.13 0.00
POST POST /v3/auth/tokens 457 0 76.86 64 102 15.23 0.00
Aggregated 9735 1 96.47 8 60000 324.50 0.03

Response Time Metrics

Method Name 50%ile (ms) 60%ile (ms) 70%ile (ms) 80%ile (ms) 90%ile (ms) 95%ile (ms) 99%ile (ms) 100%ile (ms)
DELETE DELETE /v3/auth/tokens 100 100 110 110 110 110 130 227
DELETE DELETE /v3/projects/:id (teardown) 36 36 36 52 52 52 52 52
DELETE DELETE /v3/users/:id (teardown) 50 50 50 50 76 76 76 76
GET 88 92 96 100 150 150 160 180
GET GET /v3/auth/tokens (validate new) 100 100 100 110 110 110 120 60,000
GET GET /v3/projects/:id 78 79 80 82 84 87 93 115
GET GET /v3/projects/:id (catalog) 78 79 80 82 85 87 93 108
GET GET /v3/users/:id 83 85 86 88 90 93 100 108
GET GET /v3/users/:id (catalog) 83 84 85 86 88 91 95 100
POST POST /v3/auth/tokens 76 77 79 81 83 85 91 100
Aggregated 84 87 93 98 110 150 160 60,000

Status Code Metrics

Method Name Status Codes
DELETE DELETE /v3/auth/tokens 462 [204]
DELETE DELETE /v3/projects/:id (teardown) 2 [204]
DELETE DELETE /v3/users/:id (teardown) 3 [204]
GET 4,909 [200]
GET GET /v3/auth/tokens (validate new) 1 [0], 460 [200]
GET GET /v3/projects/:id 768 [200]
GET GET /v3/projects/:id (catalog) 758 [200]
GET GET /v3/users/:id 1,071 [200]
GET GET /v3/users/:id (catalog) 844 [200]
POST POST /v3/auth/tokens 457 [200]
Aggregated 9,267 [200], 467 [204], 1 [0]

Transaction Metrics

Transaction # Times Run # Fails Average (ms) Min (ms) Max (ms) RPS Failures/s
ReadHeavy
0.0 1 0 36.00 36 36 0.03 0.00
0.1 833 0 94.95 82 130 27.77 0.00
0.2 832 0 78.57 66 102 27.73 0.00
0.3 832 0 78.52 67 111 27.73 0.00
TokenLifecycle
1.0 0 0 0.00 0 0 0.00 0.00
1.1 462 0 411.74 112 60093 15.40 0.00
ValidateToken
2.0 0 0 0.00 0 0 0.00 0.00
2.1 812 0 148.35 109 183 27.07 0.00
UserCRUD
3.0 0 0 0.00 0 0 0.00 0.00
3.1 0 0 0.00 0 0 0.00 0.00
3.2 1071 0 83.96 72 108 35.70 0.00
3.3 3 0 57.00 45 76 0.10 0.00
ProjectCRUD
4.0 0 0 0.00 0 0 0.00 0.00
4.1 0 0 0.00 0 0 0.00 0.00
4.2 768 0 78.00 66 115 25.60 0.00
4.3 2 0 44.00 36 52 0.07 0.00
UserRead
5.0 0 0 0.00 0 0 0.00 0.00
5.1 840 0 95.34 82 121 28.00 0.00
5.2 844 0 82.97 72 104 28.13 0.00
ProjectRead
6.0 0 0 0.00 0 0 0.00 0.00
6.1 760 0 79.37 68 113 25.33 0.00
6.2 758 0 78.55 67 108 25.27 0.00
Aggregated 8818 0 106.50 36 60093 293.93 0.00

Scenario Metrics

Transaction # Users # Times Run Average (ms) Min (ms) Max (ms) Scenarios/s Iterations
ReadHeavy 7 830 253.06 233 295 27.67 118.57
TokenLifecycle 5 457 282.70 262 410 15.23 91.40
ValidateToken 4 808 148.41 132 183 26.93 202.00
UserCRUD 3 1068 83.96 72 108 35.60 356.00
ProjectCRUD 2 766 78.00 66 115 25.53 383.00
UserRead 5 840 178.84 163 216 28.00 168.00
ProjectRead 4 757 158.45 142 193 25.23 189.25
Aggregated 30 5526 159.02 66 410 184.20 1508.22

Error Metrics

Method Name # Error
GET GET /v3/auth/tokens (validate new) 1 error sending request GET /v3/auth/tokens (validate new): operation timed out

View full report

@gtema gtema marked this pull request as draft July 6, 2026 12:21
@gtema gtema force-pushed the claude/scim-v2-support-adr-u1f488 branch from 3569f8f to e1f1c16 Compare July 6, 2026 13:56
Adds the realm-registration layer for SCIM v2 resource provisioning,
the first of six planned phases, plus the first fully working SCIM
resource: POST/GET/PUT/DELETE /SCIM/v2/{domain_id}/Users, backed by
real Identity users.

- New core type `ScimRealmResource` (+ Create/Update/ListParameters)
  and a `ScimRealmProviderError` enum, following the ApiClientResource
  pattern. `idp_id` is mandatory and validated against a real
  `IdentityProvider` at realm create/update.
- New `openstack-keystone-scim-driver-raft` crate storing realms at
  `scim_realm:v1:<domain_id>:<provider_id>`.
- New `ScimRealmApi`/`ScimRealmService` provider
  (ExecutionContext-based, so realm CRUD gets CADF audit trail parity
  with other v4 admin resources) wired through
  `PluginManagerApi`/`Provider`/`Config`.
- New `/v4/scim-realms` CRUD API with OPA policies under
  `identity/scim_realm/*`, requiring a `manager` (or `admin`) role.
- `ScimRealmAuth` extractor (ADR 0024 §2.C amendment to ADR 0021 §3
  Step 4): wraps the API-Key ingress pipeline, enforcing Domain-only
  scope and the Realm Activation Gate (403 if missing/disabled) before
  any SCIM resource handler runs.
- Mapping Engine write-time constraint (ADR 0024 §2.C): rejects
  `Authorization::Project` rules in a ruleset whose `provider_id` has
  an active SCIM realm, with 422 Unprocessable Entity, mirroring the
  existing `is_system` defense-in-depth check.
- `ScimResourceIndex` core type + scim-driver-raft storage, with an
  atomic realm-scoped externalId claim (create_if_absent) closing the
  TOCTOU race for ADR 0024 §3.D.
- Domain-wide case-insensitive userName collision check
  (`IdentityApi::find_user_by_name_ci`) against local_user/nonlocal_user.
- ScimRealmAuth-gated Users handlers implementing the Ownership
  Fencing Algorithm (§3.C), soft-disable delete with token revocation
  + CADF disable event (§6.A), and a SCIM-shaped 409 uniqueness error
  envelope.
- Fixed a latent bug in `resolve_verified_api_client`: it extracted
  `Path<String>` for domain_id, which only works when the route has
  exactly one dynamic segment; the new /Users/{id} routes have two and
  would have failed every request with 401.
- OPA policies under `identity/scim/user/{create,list,show,update,delete}`
  plus their `_test.rego` pairs.

SCIM provisioning and federation JIT login used to create separate
`user` rows for the same real person: SCIM always wrote a `local_user`
row while federation kept its own `federated_user` link, with nothing
correlating the two. Fixed by matching python-keystone's own shadow-user
scheme instead of inventing a new one:

- `generate_public_id(domain_id, local_id, entity_type)`
  (core::identity) is bit-compatible with python-keystone's
  `id_generators.sha256.Generator.generate_public_ID`: sha256 over
  domain_id+entity_type+local_id in that order, no `idp_id` mixed in.
  This deliberately inherits upstream's own accepted collision risk
  when multiple IdPs share a domain and reuse the same subject value,
  rather than diverging from the reference formula.
- `UserCreate.user_type` (Local/NonLocal) drives `identity-driver-sql`
  to create a `nonlocal_user` row instead of `local_user` for
  externally-managed identities; setting a password on a NonLocal user
  is a hard `Conflict`.
- SCIM user create requires `externalId`, derives the user id from it
  deterministically, and provisions as NonLocal.
- Federation JIT (`find_or_create_federated_user`) checks for a user
  at that same deterministic id before its legacy `federated_user`
  lookup/create fallback, and now assigns that id itself when creating
  a brand-new federated user too — so convergence holds regardless of
  which side (SCIM or federation) provisions the person first.
- SCIM create pre-checks for an existing user at the derived id and
  returns a clean 409 (`scimType: uniqueness`) instead of letting a
  primary-key collision surface as an opaque driver error.

Assisted-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
@gtema gtema force-pushed the claude/scim-v2-support-adr-u1f488 branch from e1f1c16 to 6bbcec4 Compare July 6, 2026 14:23
@gtema gtema marked this pull request as ready for review July 6, 2026 15:10
@gtema gtema merged commit 090057e into main Jul 6, 2026
39 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant