feat(AF-621): SCIM 2.0 provisioning — IdP-driven user & group lifecycle - #721
Merged
Conversation
…dEvent
Deactivation (admin update, admin delete, and soon SCIM) now publishes
core.events.UserDeactivatedEvent on the true->false transition. The
security module revokes all refresh tokens (moved out of the
controller), and the access module revokes the user's APPROVED JIT
grants through the existing revocation path. Fixes the gap where
PUT /admin/users/{id} active=false revoked nothing.
V137-V141: scim_config + scim_tokens tables, users/user_groups scim_external_id (+ per-org partial unique indexes), users.updated_at, SCIM values on user_group_membership_source and auth_provider_type. Core: ExternalUserDirectoryService (system-actor create/update/find/ offset-list, quota + uniqueness guards, deactivation event), source- scoped group member ops for group-centric SCIM sync, and a fix so SSO-login IDP sync neither wipes nor PK-collides with SCIM-sourced memberships.
New scim Modulith module: /scim/v2 Users + Groups + discovery endpoints behind an @order(0) bearer-token filter chain (per-org tokens, SHA-256 at rest, shown once; per-request enabled/org-disabled checks; SCIM error envelope, never ProblemDetail). Hand-rolled RFC 7644 subset: eq-filters, startIndex/count, PUT + PatchOp incl. Okta and Entra payload shapes. SCIM writes only its owned attributes - never role, platform_admin, password, or TOTP. Admin surface /api/v1/admin/scim-* gated by SSO_CONFIGURE. New SCIM_* audit actions; admin-facing i18n keys in all seven locales.
/admin/scim (SSO_CONFIGURE): enabled switch, attribute-mapping selects, default role, copyable /scim/v2 base URL, and bearer-token management with a show-once modal. AuthProvider union + labels gain SCIM. Website auth chapter gets the cfg-scim operator guide (Okta + Entra ID setup, troubleshooting) with the docs.ts <-> app.js anchor contract and sitemap/JSON-LD freshness bumped. All seven locales translated.
Drives /admin/scim (enable + save, show-once token modal, revoke) and exercises the SCIM protocol against the backend origin with the issued bearer token: Okta-shaped create + userName filter, Entra-shaped PATCH deactivate, and the 401 SCIM error envelope after revocation.
07-security: SCIM section (token model, filter chain, write boundary, deactivation fan-out, failure modes, audit). 03-data-model: scim_config + scim_tokens tables, users/user_groups SCIM columns, membership source and audit values. 05-backend: scim module + UserDeactivatedEvent flow. 02/06/12, CLAUDE.md module tree, README, website homepage tiles + source map, freshness markers.
Review findings: window.location.origin points at the SPA, which proxies nothing — the copyable base URL now uses apiBaseUrl() (same trap OAuth2ConfigPage documents). Also: UsersPage provider filter and the enumLabels exhaustiveness test gain SCIM, the token list renders a real error state instead of 'no tokens yet', and the e2e cleanup rationale is documented.
Backend: refresh-token revocation is synchronous again at both deactivation services (the in-memory event channel is fire-and-forget; the redundant security listener is removed and the event now owns only JIT-grant fan-out), SCIM orchestrator writes are @transactional so a PatchOp applies as a unit, the SCIM error handler maps malformed bodies/ids to 400 instead of 500, authenticate() no longer flushes the last-used bump outside its guard, and updated_at is stamped on the returned view. Frontend: GroupMembershipSource union/tag/labels gain SCIM (x7 locales), ProfilePage treats SCIM accounts as external. Docs: the membership-isolation claim now matches the code.
Contributor
Frontend Test Results1 480 tests 1 480 ✅ 3m 27s ⏱️ Results for commit 76576f3. ♻️ This comment has been updated with latest results. |
Contributor
Coverage Report for Frontend Coverage (frontend)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
Contributor
Backend Test Results6 191 tests +125 6 191 ✅ +125 19m 52s ⏱️ + 2m 5s Results for commit 76576f3. ± Comparison against base commit 572a8fe. This pull request removes 1 and adds 126 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
AntD's modal X icon also carries the accessible name 'Close' — the strict-mode locator resolved to two elements in CI.
Wait for the show-once modal to hide after Close (AntD keeps it mounted through the close animation) and scope the token-name assertion to the Bearer tokens table so it cannot double-match the modal's copy.
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #621
What
A SCIM 2.0 provisioning server so identity providers (Okta, Microsoft Entra ID, Keycloak, OneLogin) drive user and group lifecycle — create, update, deactivate, group sync — instead of admins doing it by hand after SSO.
scimSpring Modulith module —/scim/v2/Users,/Groups, and discovery endpoints behind a dedicated@Order(0)SecurityFilterChainauthenticated by long-lived per-org bearer tokens (af_scim_…, SHA-256 at rest, shown once; per-requestenabled+ org-disabled checks; SCIM error envelope, never ProblemDetail). Hand-rolled RFC 7644 pragmatic subset:eqfilters,startIndex/count, PUT + PatchOp covering both Okta and Entra payload shapes. SCIM writes only its owned attributes — never roles,platform_admin, passwords, or TOTP.activetrue→false transition (admin API or SCIM) revokes refresh tokens synchronously and publishescore.events.UserDeactivatedEvent; the access module revokes the user's APPROVED JIT grants through the ordinary revocation path. This also fixes a pre-existing gap wherePUT /admin/users/{id}withactive=falserevoked nothing.SCIMsource, disjoint from adminMANUALand SSO-loginIDProws; the SSO sync was fixed to neither wipe nor PK-collide with SCIM rows.scim_config,scim_tokens,users/user_groups.scim_external_id(+ per-org partial unique indexes),users.updated_at,SCIMenum values (with.sql.confsidecars)./admin/scim(SSO_CONFIGURE): enabled switch, attribute mapping, default role, copyable API-base/scim/v2URL, token table with show-once creation modal. All seven locales translated (frontend + backend keys).e2e/tests/admin-scim-config.spec.ts: config CRUD, show-once token, Okta-shaped provision + filter, Entra-shaped deactivation, 401 envelope after revocation.Docs updated
docs/02-architecture.md,docs/03-data-model.md,docs/04-api-spec.md(spec-first),docs/05-backend.md,docs/06-frontend.md,docs/07-security.md,docs/12-roadmap.md,CLAUDE.md,README.md, and the website:website/docs/configuration/auth/index.html(#cfg-scimoperator guide with Okta/Entra walkthroughs),website/index.html,website/app.js,website/sitemap.xml,website/README.md.Deliberate exceptions, called out per convention:
/scim/v2/UsersPascalCase paths are RFC 7644-mandated (documented indocs/04-api-spec.md); SCIM protocol error strings are not localized (machine consumer). No new env vars — configuration is DB rows, so nodocs/09-deployment.mdchange.Review notes
Independent reviewers (af-reviewer, af-java-reviewer, af-frontend-reviewer) ran before this PR; every Blocker and all actionable Concerns were fixed in the last two commits (IdP-facing base URL from the API base,
GroupMembershipSource/AuthProviderfrontend fan-out, synchronous refresh-token revocation, transactional PatchOp, SCIM 400s for malformed bodies). Surviving items, deliberately not addressed:UserDeactivationGrantRevokeris an in-memory@ApplicationModuleListener; a crash between commit and listener execution drops the revocation (refresh-token revocation is synchronous and unaffected). A durable fix is the Spring Modulith JPA event-publication registry — proposed as a follow-up rather than growing this PR.count=0is clamped to 1 on SCIM list endpoints (RFC 7644 §3.4.2.4 says "total only, no resources") — none of the supported IdPs sendcount=0.docs/03-data-model.mdinstead of restricting the admin API.AuditLogPagefilter arrays omit the newSCIM_*actions, matching the existing curated-subset convention (SAML_CONFIG_UPDATEDis absent too).Verification
mvn verify(full unit + integration, Testcontainers) — green; architecture gates (ApplicationModulesTest,ApiPackageDependencyTest,MessagesParityTest) green; affected tests re-run after review fixes (117 tests green).lint/typecheck/test:coverage(94% lines / 84% branches) /build— green.admin-saml-configtemplate and will run in the CIe2ejob.