From a1d3c6eb29406b61fea13c832eca94f9434d9060 Mon Sep 17 00:00:00 2001 From: Alex Morel Date: Mon, 4 Nov 2024 17:48:05 +0100 Subject: [PATCH] Provide SCIM2 client capabilities behing an experimental Feature Profile Closes #1234 Signed-off-by: Alex Morel --- .../java/org/keycloak/common/Profile.java | 2 + dependencies/server-all/pom.xml | 5 + .../images/scim-event-listener-page.png | Bin 0 -> 83173 bytes .../images/scim-federation-provider-page.png | Bin 0 -> 107002 bytes .../topics/user-federation/scim.adoc | 72 +++++ federation/pom.xml | 1 + federation/scim/pom.xml | 83 ++++++ .../sh/libre/scim/core/ScimDispatcher.java | 171 +++++++++++ ...ntConfigurationStorageProviderFactory.java | 165 ++++++++++ .../scim/core/ScrimEndPointConfiguration.java | 101 +++++++ .../InconsistentScimMappingException.java | 7 + ...alidResponseFromScimEndpointException.java | 29 ++ .../core/exceptions/RollbackApproach.java | 55 ++++ .../core/exceptions/RollbackStrategy.java | 22 ++ .../core/exceptions/ScimExceptionHandler.java | 43 +++ .../exceptions/ScimPropagationException.java | 12 + .../core/exceptions/SkipOrStopApproach.java | 59 ++++ .../core/exceptions/SkipOrStopStrategy.java | 66 ++++ .../UnexpectedScimDataException.java | 7 + .../core/service/AbstractScimService.java | 281 ++++++++++++++++++ .../core/service/EntityOnRemoteScimId.java | 6 + .../scim/core/service/GroupScimService.java | 131 ++++++++ .../libre/scim/core/service/KeycloakDao.java | 81 +++++ .../libre/scim/core/service/KeycloakId.java | 7 + .../libre/scim/core/service/ScimClient.java | 155 ++++++++++ .../scim/core/service/ScimResourceType.java | 29 ++ .../scim/core/service/UserScimService.java | 145 +++++++++ .../ScimBackgroundGroupMembershipUpdater.java | 74 +++++ .../scim/event/ScimEventListenerProvider.java | 252 ++++++++++++++++ .../ScimEventListenerProviderFactory.java | 36 +++ .../sh/libre/scim/jpa/ScimResourceDao.java | 96 ++++++ .../sh/libre/scim/jpa/ScimResourceId.java | 83 ++++++ .../libre/scim/jpa/ScimResourceMapping.java | 89 ++++++ .../libre/scim/jpa/ScimResourceProvider.java | 29 ++ .../scim/jpa/ScimResourceProviderFactory.java | 39 +++ .../META-INF/jboss-deployment-structure.xml | 10 + .../META-INF/scim-resource-changelog.xml | 35 +++ ...pa.entityprovider.JpaEntityProviderFactory | 1 + ...ycloak.events.EventListenerProviderFactory | 1 + ...eycloak.storage.UserStorageProviderFactory | 1 + pom.xml | 5 + quarkus/runtime/pom.xml | 10 + 42 files changed, 2496 insertions(+) create mode 100644 docs/documentation/server_admin/images/scim-event-listener-page.png create mode 100644 docs/documentation/server_admin/images/scim-federation-provider-page.png create mode 100644 docs/documentation/server_admin/topics/user-federation/scim.adoc create mode 100644 federation/scim/pom.xml create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/ScimDispatcher.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/AbstractScimService.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/GroupScimService.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakDao.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakId.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/ScimClient.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/ScimResourceType.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/core/service/UserScimService.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceId.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java create mode 100644 federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java create mode 100644 federation/scim/src/main/resources/META-INF/jboss-deployment-structure.xml create mode 100644 federation/scim/src/main/resources/META-INF/scim-resource-changelog.xml create mode 100644 federation/scim/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory create mode 100644 federation/scim/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory create mode 100644 federation/scim/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index b3551549da..3107930411 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -112,6 +112,8 @@ public class Profile { OID4VC_VCI("Support for the OID4VCI protocol as part of OID4VC.", Type.EXPERIMENTAL), + SCIM("Synchronise users and groups with registered SCIM endpoints", Type.EXPERIMENTAL), + OPENTELEMETRY("OpenTelemetry Tracing", Type.PREVIEW), DECLARATIVE_UI("declarative ui spi", Type.EXPERIMENTAL), diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index c25827c2a3..b9e6bc1ef0 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -70,6 +70,11 @@ org.keycloak keycloak-kerberos-federation + + + org.keycloak + keycloak-scim-federation + org.keycloak diff --git a/docs/documentation/server_admin/images/scim-event-listener-page.png b/docs/documentation/server_admin/images/scim-event-listener-page.png new file mode 100644 index 0000000000000000000000000000000000000000..5c45734c68fcc9ecfe558cd84fb571c8fcffb20f GIT binary patch literal 83173 zcmdSAcT`jB);?<4h*Gx&P^w6iE=ZBCqG0F(p+lsX01-lwjs;Kpd!6V zml}~yfJleXelOoS?sLZd>yGb^JMOq^L>I~G?>px+pZUzW-f3wlQ&TWfoH=ub8uH|k z_L(!}zGu#ykGgmP{Bna~;1I0-LOz7(UIdHpMXR@`Kf66PaMMB9xOtkpTA#6XMmSju zA}w95t(}o}2sh&SMtQK)?bDqUU9HXC>=Dj4b?u$3&p4U83f&YHy@{}N1M54YVmI$d zNsEg~i`|j6*_JtT=H?m5qX)V#QdTBC4dUk5TGxATF#mOy>@4}(OCNZ|pxS_Tr-~|* zO>Uwfe{vFaMZenJPFRz`xI=DUoxWY-JnKCqF+A&H%KqW;XXNZ1DmuF5qk18sX{lYL z!mbqgU|8?w(NtbR!EHrwd;eYneOrCe41d0Xb*&P}$KK&vubV8t8wR31A_Xt|IFxtYxLbupJCO}v0=qv4kSi3E#H11FGjK^$;$$w6f<2+fvbhL|4Xt(Ov zu50@GRm`%5N^bqzRT6k+SHI);c1tOp#d<|xI9;7^VCv}v4DO7~pa16!$DK)%h?2`3 zN+waIBb^pyc3X>m4t-h8bPd4_FA+lgruCHq^r|I2hB)7SlT_M&j@uIs4Grz5RXF^R zT^4M<_&rl`tkOOv{;=**kv?Yl_~;-mDJf}&;I}*J33jX>oSSch+Ald)89{5DO%uE8 zVuXEUS}oWWU^8uqCzDUo$tNbxWiPD?E#nS7}9$Y^rP~;UeQ;2jU6V-ic#IHN0(OdjVGbS zY<~vnr^>r`@1BOxu(&l*OBSfa@kQr!CriSozXm0VSO(LuNV|pWzYS>k{P{jLqu6a_ zOML;@G^Nk^^XDrjd)!A#O!-V|OGB^mZbTw!7{#7k<ee>o|3b`smS--~D~`^EK^Ysk!2fmi>hOIam8{@f?f|n{PM^{hP~;=uAwaw=^DwN?*v$-DOIp zpG1nSCKovbDt~!kFV_T5ir#kn3BmgUlcczK@x#DUY_r9BLp*0Vvy`@Tjd7&Haczo5 zz8ZJT1zOf(^<;_taG#$q$pmW6t8D9VEJG`g=vMd~)Gj<*4KE2X;j(iDc_C12^Cv z)Xev^qOHmd2Ej-S6+BgZ`SK;s;|DkeI1%ubqVc}S*`FZHXzo#=WASff_rh3Y~r)6bj z;53X1{-1FB^GvJbHJZ`7C|%y|#Xd#Tdbg-*>Ft}PUc&GCE+B5gTUf&J$Is3xAu9mrV|AAKob47>vx`b^hSA2hb*peC z-YUDkTLbG(mu$Bt*DBSM`5A;3DSe8_ z80&fZ`3C9|j-??iIjFqw$6 z4`mQ8iH*JX_|c=i<6B)nR1SJ%h+C6Ve={=+Ko}()KXUTky7e%Kk|Eyw`spB2ozBe0 z3t8|vwPyzhRF_Q!#=>WRW6t06mhAp)45PS1EM6Rp>e9h9L_=e^+`5w!(%0ALz23~= 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 ZrpIsX$1*sAv&nl>BrlwOdiv7M{{kbBmK^{9 literal 0 HcmV?d00001 diff --git a/docs/documentation/server_admin/topics/user-federation/scim.adoc b/docs/documentation/server_admin/topics/user-federation/scim.adoc new file mode 100644 index 0000000000..024ec4ed47 --- /dev/null +++ b/docs/documentation/server_admin/topics/user-federation/scim.adoc @@ -0,0 +1,72 @@ +[[_scim]] + +=== SCIM client capabilities + +{project_name} includes a http://www.simplecloud.info[SCIM2] client allowing to : + +* Declare SCIM endpoints (through the identity federation UI). Any tool implementing SCIM protocol can be wired to the +{project_name} instance through this declaration. +* Propagate users and groups from {project_name} to SCIM endpoints : when a user/group gets created or modified in {project_name}, +the modification is forwarded to all declared SCIM endpoints through SCIM calls within the transaction scope. If +propagation fails, changes can be rolled back or not according to a configurable rollback strategy. +* Synchronize users and groups from SCIM endpoints (through the {project_name} synchronization mechanism). + +See [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) +and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)) for further details + +==== Enabling SCIM extension + +[NOTE] +==== +This extension is currently in experimental mode, and requires the ```SCIM``` experimental Profile to be enabled +==== + +.Procedure +. Click on *Admin Console > Realm Settings > Events* in the menu. +. Add `scim` to the list of event listeners +image:images/scim-event-listener-page.png[Enable SCIM Event listeners] +. Save + +==== Registering SCIM Service Providers + +.Procedure +. Click on *User federation > Add Scim Providers* +image:images/scim-federation-provider-page.png[Configure SCIM service provider] +. Fill required fields according to the SCIM endpoint you are wiring +. If you enable import during sync then you can choose between to following import actions: + +- Create Local - adds users to keycloak +- Nothing +- Delete Remote - deletes users from the remote application + +==== Sync + +You can set up a periodic sync for all users or just changed users - it's not mandatory. You can either do: + +- Periodic Full Sync +- Periodic Changed User Sync + + +==== Technical notes + +===== Motivation + +We want to build a unified collaborative platform based on multiple applications. To do that, we need a way to propagate +immediately changes made in Keycloak to all these applications. And we want to keep using OIDC or SAML as the +authentication protocol. + +This will allow users to collaborate seamlessly across the platform without requiring every user to have connected once +to each application. This will also ease GDRP compliance because deleting a user in Keycloak will delete the user from +every app. The SCIM protocol is standard, comprehensible and easy to implement. It's a perfect fit for our goal. + +We chose to build application extensions/plugins because it's easier to deploy and thus will benefit to a larger portion +of the FOSS community. + +===== Keycloak specific + +This extension uses 3 concepts in KeyCloak : + +- Event Listener : used to listen for changes within Keycloak (e.g. User creation, Group deletion...) and propagate +them to registered SCIM service providers through SCIM requests. +- Federation Provider : used to set up all the SCIM service providers endpoint without creating our own UI. +- JPA Entity Provider : used to save the mapping between the local IDs and the service providers IDs. diff --git a/federation/pom.xml b/federation/pom.xml index 314e274a73..74edc9ae82 100755 --- a/federation/pom.xml +++ b/federation/pom.xml @@ -36,6 +36,7 @@ kerberos ldap sssd + scim diff --git a/federation/scim/pom.xml b/federation/scim/pom.xml new file mode 100644 index 0000000000..2ec0300183 --- /dev/null +++ b/federation/scim/pom.xml @@ -0,0 +1,83 @@ + + + + keycloak-parent + org.keycloak + 999.0.0-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-scim-federation + Keycloak Federation from SCIM endpoints + + This extension add SCIM2 client capabilities to Keycloak. + It allows to : + * Declare SCIM endpoints (through the identity federation UI). Any tool implementing SCIM protocol can be wired to the + Keycloak instance through this declaration. + * Propagate users and groups from Keycloak to SCIM endpoints : when a user/group gets created or modified in Keycloak, + the modification is forwarded to all declared SCIM endpoints through SCIM calls within the transaction scope. If + propagation fails, changes can be rolled back or not according to a configurable rollback strategy. + * Import users and groups from SCIM endpoints (through the Keycloak synchronization mechanism). + See [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) + and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)) for further details + + + + 1.26.0 + 2.2.0 + 2.0.2.Final + + + + org.keycloak + keycloak-core + provided + + + org.keycloak + keycloak-server-spi + provided + + + org.keycloak + keycloak-server-spi-private + provided + + + org.keycloak + keycloak-services + provided + + + org.keycloak + keycloak-model-jpa + provided + + + org.jboss.logging + jboss-logging + provided + + + io.github.resilience4j + resilience4j-retry + ${r4j-version} + provided + + + de.captaingoldfish + scim-sdk-common + ${scim-sdk-version} + provided + + + de.captaingoldfish + scim-sdk-client + ${scim-sdk-version} + provided + + + + diff --git a/federation/scim/src/main/java/sh/libre/scim/core/ScimDispatcher.java b/federation/scim/src/main/java/sh/libre/scim/core/ScimDispatcher.java new file mode 100644 index 0000000000..d3d675108b --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/ScimDispatcher.java @@ -0,0 +1,171 @@ +package sh.libre.scim.core; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import sh.libre.scim.core.exceptions.ScimExceptionHandler; +import sh.libre.scim.core.exceptions.ScimPropagationException; +import sh.libre.scim.core.exceptions.SkipOrStopApproach; +import sh.libre.scim.core.exceptions.SkipOrStopStrategy; +import sh.libre.scim.core.service.AbstractScimService; +import sh.libre.scim.core.service.GroupScimService; +import sh.libre.scim.core.service.UserScimService; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * In charge of sending SCIM Request to all registered Scim endpoints. + */ +public class ScimDispatcher { + + private static final Logger LOGGER = Logger.getLogger(ScimDispatcher.class); + + private final KeycloakSession session; + private final ScimExceptionHandler exceptionHandler; + private final SkipOrStopStrategy skipOrStopStrategy; + private boolean clientsInitialized = false; + private final List userScimServices = new ArrayList<>(); + private final List groupScimServices = new ArrayList<>(); + + + public ScimDispatcher(KeycloakSession session) { + this.session = session; + this.exceptionHandler = new ScimExceptionHandler(session); + // By default, use a permissive Skip or Stop strategy + this.skipOrStopStrategy = SkipOrStopApproach.ALWAYS_SKIP_AND_CONTINUE; + } + + /** + * Lists all active ScimStorageProviderFactory and create new ScimClients for each of them + */ + public void refreshActiveScimEndpoints() { + // Step 1: close existing clients (as configuration may have changed) + groupScimServices.forEach(GroupScimService::close); + groupScimServices.clear(); + userScimServices.forEach(UserScimService::close); + userScimServices.clear(); + + // Step 2: Get All SCIM endpoints defined in Admin Console (enabled ScimStorageProviderFactory) + session.getContext().getRealm().getComponentsStream() + .filter(m -> ScimEndpointConfigurationStorageProviderFactory.ID.equals(m.getProviderId()) + && m.get("enabled", true)) + .forEach(scimEndpointConfigurationRaw -> { + try { + ScrimEndPointConfiguration scrimEndPointConfiguration = new ScrimEndPointConfiguration(scimEndpointConfigurationRaw); + + // Step 3 : create scim clients for each endpoint + if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP, false)) { + GroupScimService groupScimService = new GroupScimService(session, scrimEndPointConfiguration, skipOrStopStrategy); + groupScimServices.add(groupScimService); + } + if (scimEndpointConfigurationRaw.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER, false)) { + UserScimService userScimService = new UserScimService(session, scrimEndPointConfiguration, skipOrStopStrategy); + userScimServices.add(userScimService); + } + } catch (IllegalArgumentException e) { + if (skipOrStopStrategy.allowInvalidEndpointConfiguration()) { + LOGGER.warn("[SCIM] Invalid Endpoint configuration " + scimEndpointConfigurationRaw.getId(), e); + } else { + throw e; + } + } + }); + } + + public void dispatchUserModificationToAll(SCIMPropagationConsumer operationToDispatch) { + initializeClientsIfNeeded(); + Set servicesCorrectlyPropagated = new LinkedHashSet<>(); + userScimServices.forEach(userScimService -> { + try { + operationToDispatch.acceptThrows(userScimService); + servicesCorrectlyPropagated.add(userScimService); + } catch (ScimPropagationException e) { + exceptionHandler.handleException(userScimService.getConfiguration(), e); + } + }); + // TODO we could iterate on servicesCorrectlyPropagated to undo modification on already handled SCIM endpoints + LOGGER.infof("[SCIM] User operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size()); + } + + public void dispatchGroupModificationToAll(SCIMPropagationConsumer operationToDispatch) { + initializeClientsIfNeeded(); + Set servicesCorrectlyPropagated = new LinkedHashSet<>(); + groupScimServices.forEach(groupScimService -> { + try { + operationToDispatch.acceptThrows(groupScimService); + servicesCorrectlyPropagated.add(groupScimService); + } catch (ScimPropagationException e) { + exceptionHandler.handleException(groupScimService.getConfiguration(), e); + } + }); + // TODO we could iterate on servicesCorrectlyPropagated to undo modification on already handled SCIM endpoints + LOGGER.infof("[SCIM] Group operation dispatched to %d SCIM server", servicesCorrectlyPropagated.size()); + } + + public void dispatchUserModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer operationToDispatch) { + initializeClientsIfNeeded(); + // Scim client should already have been created + Optional matchingClient = userScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); + if (matchingClient.isPresent()) { + try { + operationToDispatch.acceptThrows(matchingClient.get()); + LOGGER.infof("[SCIM] User operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName()); + } catch (ScimPropagationException e) { + exceptionHandler.handleException(matchingClient.get().getConfiguration(), e); + } + } else { + LOGGER.error("[SCIM] Could not find a Scim Client matching User endpoint configuration" + scimServerConfiguration.getId()); + } + } + + + public void dispatchGroupModificationToOne(ComponentModel scimServerConfiguration, SCIMPropagationConsumer operationToDispatch) { + initializeClientsIfNeeded(); + // Scim client should already have been created + Optional matchingClient = groupScimServices.stream().filter(u -> u.getConfiguration().getId().equals(scimServerConfiguration.getId())).findFirst(); + if (matchingClient.isPresent()) { + try { + operationToDispatch.acceptThrows(matchingClient.get()); + LOGGER.infof("[SCIM] Group operation dispatched to SCIM server %s", matchingClient.get().getConfiguration().getName()); + } catch (ScimPropagationException e) { + exceptionHandler.handleException(matchingClient.get().getConfiguration(), e); + } + } else { + LOGGER.error("[SCIM] Could not find a Scim Client matching Group endpoint configuration" + scimServerConfiguration.getId()); + } + } + + public void close() { + for (GroupScimService c : groupScimServices) { + c.close(); + } + for (UserScimService c : userScimServices) { + c.close(); + } + groupScimServices.clear(); + userScimServices.clear(); + } + + private void initializeClientsIfNeeded() { + if (!clientsInitialized) { + clientsInitialized = true; + refreshActiveScimEndpoints(); + } + } + + /** + * A Consumer that throws ScimPropagationException. + * + * @param An {@link AbstractScimService to call} + */ + @FunctionalInterface + public interface SCIMPropagationConsumer { + + void acceptThrows(T elem) throws ScimPropagationException; + + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java b/federation/scim/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java new file mode 100644 index 0000000000..b73df47725 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/ScimEndpointConfigurationStorageProviderFactory.java @@ -0,0 +1,165 @@ +package sh.libre.scim.core; + +import de.captaingoldfish.scim.sdk.common.constants.HttpHeader; +import jakarta.ws.rs.core.MediaType; +import org.apache.commons.lang3.BooleanUtils; +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.UserStorageProviderFactory; +import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.user.ImportSynchronization; +import org.keycloak.storage.user.SynchronizationResult; +import sh.libre.scim.event.ScimBackgroundGroupMembershipUpdater; + +import java.util.Date; +import java.util.List; + +/** + * Allows to register and configure Scim endpoints through Admin console, using the provided config properties. + */ +public class ScimEndpointConfigurationStorageProviderFactory + implements UserStorageProviderFactory, ImportSynchronization { + public static final String ID = "scim"; + private static final Logger LOGGER = Logger.getLogger(ScimEndpointConfigurationStorageProviderFactory.class); + + @Override + public String getId() { + return ID; + } + + + @Override + public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, + UserStorageProviderModel model) { + // Manually Launch a synchronization between keycloack and the SCIM endpoint described in the given model + LOGGER.infof("[SCIM] Sync from ScimStorageProvider - Realm %s - Model %s", realmId, model.getName()); + SynchronizationResult result = new SynchronizationResult(); + KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { + RealmModel realm = session.realms().getRealm(realmId); + session.getContext().setRealm(realm); + ScimDispatcher dispatcher = new ScimDispatcher(session); + if (BooleanUtils.TRUE.equals(model.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER))) { + dispatcher.dispatchUserModificationToOne(model, client -> client.sync(result)); + } + if (BooleanUtils.TRUE.equals(model.get(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP))) { + dispatcher.dispatchGroupModificationToOne(model, client -> client.sync(result)); + } + dispatcher.close(); + }); + return result; + } + + @Override + public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, + UserStorageProviderModel model) { + return this.sync(sessionFactory, realmId, model); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + ScimBackgroundGroupMembershipUpdater scimBackgroundGroupMembershipUpdater = new ScimBackgroundGroupMembershipUpdater(factory); + scimBackgroundGroupMembershipUpdater.startBackgroundUpdates(); + } + + @Override + public List getConfigProperties() { + // These Config Properties will be use to generate configuration page in Admin Console + return ProviderConfigurationBuilder.create() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_ENDPOINT) + .type(ProviderConfigProperty.STRING_TYPE) + .required(true) + .label("SCIM 2.0 endpoint") + .helpText("External SCIM 2.0 base " + + "URL (/ServiceProviderConfig /Schemas and /ResourcesTypes should be accessible)") + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_CONTENT_TYPE) + .type(ProviderConfigProperty.LIST_TYPE) + .label("Endpoint content type") + .helpText("Only used when endpoint doesn't support application/scim+json") + .options(MediaType.APPLICATION_JSON, HttpHeader.SCIM_CONTENT_TYPE) + .defaultValue(HttpHeader.SCIM_CONTENT_TYPE) + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_MODE) + .type(ProviderConfigProperty.LIST_TYPE) + .label("Auth mode") + .helpText("Select the authorization mode") + .options("NONE", "BASIC_AUTH", "BEARER") + .defaultValue("NONE") + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_USER) + .type(ProviderConfigProperty.STRING_TYPE) + .label("Auth username") + .helpText("Required for basic authentication.") + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_AUTH_PASSWORD) + .type(ProviderConfigProperty.PASSWORD) + .label("Auth password/token") + .helpText("Password or token required for basic or bearer authentication.") + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_USER) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .label("Enable user propagation") + .helpText("Should operation on users be propagated to this provider?") + .defaultValue(BooleanUtils.TRUE) + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_PROPAGATION_GROUP) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .label("Enable group propagation") + .helpText("Should operation on groups be propagated to this provider?") + .defaultValue(BooleanUtils.TRUE) + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .label("Enable import during sync") + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_IMPORT_ACTION) + .type(ProviderConfigProperty.LIST_TYPE) + .label("Import action") + .helpText("What to do when the user doesn't exists in Keycloak.") + .options("NOTHING", "CREATE_LOCAL", "DELETE_REMOTE") + .defaultValue("CREATE_LOCAL") + .add() + .property() + .name(ScrimEndPointConfiguration.CONF_KEY_SYNC_REFRESH) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .label("Enable refresh during sync") + .name(ScrimEndPointConfiguration.CONF_KEY_LOG_ALL_SCIM_REQUESTS) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .label("Log SCIM requests and responses") + .helpText("If true, all sent SCIM requests and responses will be logged") + .add() + .build(); + } + + + @Override + public ScimEndpointConfigurationStorageProvider create(KeycloakSession session, ComponentModel model) { + return new ScimEndpointConfigurationStorageProvider(); + } + + /** + * Empty implementation : we used this {@link ScimEndpointConfigurationStorageProviderFactory} to generate Admin Console page. + */ + public static final class ScimEndpointConfigurationStorageProvider implements UserStorageProvider { + @Override + public void close() { + // Nothing to close here + } + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java b/federation/scim/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java new file mode 100644 index 0000000000..6359b57153 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/ScrimEndPointConfiguration.java @@ -0,0 +1,101 @@ +package sh.libre.scim.core; + +import de.captaingoldfish.scim.sdk.client.http.BasicAuth; +import org.keycloak.component.ComponentModel; + +public class ScrimEndPointConfiguration { + // Configuration keys : also used in Admin Console page + public static final String CONF_KEY_AUTH_MODE = "auth-mode"; + public static final String CONF_KEY_AUTH_PASSWORD = "auth-pass"; + public static final String CONF_KEY_AUTH_USER = "auth-user"; + public static final String CONF_KEY_CONTENT_TYPE = "content-type"; + public static final String CONF_KEY_ENDPOINT = "endpoint"; + public static final String CONF_KEY_SYNC_IMPORT_ACTION = "sync-import-action"; + public static final String CONF_KEY_SYNC_IMPORT = "sync-import"; + public static final String CONF_KEY_SYNC_REFRESH = "sync-refresh"; + public static final String CONF_KEY_PROPAGATION_USER = "propagation-user"; + public static final String CONF_KEY_PROPAGATION_GROUP = "propagation-group"; + public static final String CONF_KEY_LOG_ALL_SCIM_REQUESTS = "log-all-scim-requests"; + + private final String endPoint; + private final String id; + private final String name; + private final String contentType; + private final String authorizationHeaderValue; + private final ImportAction importAction; + private final boolean pullFromScimSynchronisationActivated; + private final boolean pushToScimSynchronisationActivated; + private final boolean logAllScimRequests; + + public ScrimEndPointConfiguration(ComponentModel scimProviderConfiguration) { + try { + AuthMode authMode = AuthMode.valueOf(scimProviderConfiguration.get(CONF_KEY_AUTH_MODE)); + + authorizationHeaderValue = switch (authMode) { + case BEARER -> "Bearer " + scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD); + case BASIC_AUTH -> { + BasicAuth basicAuth = BasicAuth.builder() + .username(scimProviderConfiguration.get(CONF_KEY_AUTH_USER)) + .password(scimProviderConfiguration.get(CONF_KEY_AUTH_PASSWORD)) + .build(); + yield basicAuth.getAuthorizationHeaderValue(); + } + case NONE -> ""; + }; + contentType = scimProviderConfiguration.get(CONF_KEY_CONTENT_TYPE, ""); + endPoint = scimProviderConfiguration.get(CONF_KEY_ENDPOINT, ""); + id = scimProviderConfiguration.getId(); + name = scimProviderConfiguration.getName(); + importAction = ImportAction.valueOf(scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT_ACTION)); + pullFromScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_IMPORT, false); + pushToScimSynchronisationActivated = scimProviderConfiguration.get(CONF_KEY_SYNC_REFRESH, false); + logAllScimRequests = scimProviderConfiguration.get(CONF_KEY_LOG_ALL_SCIM_REQUESTS, false); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("authMode '" + scimProviderConfiguration.get(CONF_KEY_AUTH_MODE) + "' is not supported"); + } + } + + public boolean isPushToScimSynchronisationActivated() { + return pushToScimSynchronisationActivated; + } + + public boolean isPullFromScimSynchronisationActivated() { + return pullFromScimSynchronisationActivated; + } + + public String getContentType() { + return contentType; + } + + public String getAuthorizationHeaderValue() { + return authorizationHeaderValue; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public ImportAction getImportAction() { + return importAction; + } + + public String getEndPoint() { + return endPoint; + } + + public boolean isLogAllScimRequests() { + return logAllScimRequests; + } + + public enum AuthMode { + BEARER, BASIC_AUTH, NONE + } + + public enum ImportAction { + CREATE_LOCAL, DELETE_REMOTE, NOTHING + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java new file mode 100644 index 0000000000..44f7eb46e6 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/InconsistentScimMappingException.java @@ -0,0 +1,7 @@ +package sh.libre.scim.core.exceptions; + +public class InconsistentScimMappingException extends ScimPropagationException { + public InconsistentScimMappingException(String message) { + super(message); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java new file mode 100644 index 0000000000..079443622b --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/InvalidResponseFromScimEndpointException.java @@ -0,0 +1,29 @@ +package sh.libre.scim.core.exceptions; + +import de.captaingoldfish.scim.sdk.client.response.ServerResponse; + +import java.util.Optional; + +public class InvalidResponseFromScimEndpointException extends ScimPropagationException { + + private final transient Optional response; + + public InvalidResponseFromScimEndpointException(ServerResponse response, String message) { + super(message); + this.response = Optional.of(response); + } + + public InvalidResponseFromScimEndpointException(String message, Exception e) { + super(message, e); + this.response = Optional.empty(); + } + + + /** + * Empty response can occur if a major exception was thrown while retrying the request. + */ + public Optional getResponse() { + return response; + } + +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java new file mode 100644 index 0000000000..d1fb108930 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackApproach.java @@ -0,0 +1,55 @@ +package sh.libre.scim.core.exceptions; + +import com.google.common.collect.Lists; +import sh.libre.scim.core.ScrimEndPointConfiguration; + +import java.util.ArrayList; + + +public enum RollbackApproach implements RollbackStrategy { + ALWAYS_ROLLBACK { + @Override + public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) { + return true; + } + }, + NEVER_ROLLBACK { + @Override + public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) { + return false; + } + }, + CRITICAL_ONLY_ROLLBACK { + @Override + public boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e) { + if (e instanceof InconsistentScimMappingException) { + // Occurs when mapping between a SCIM resource and a keycloak user failed (missing, ambiguous..) + // Log can be sufficient here, no rollback required + return false; + } + if (e instanceof UnexpectedScimDataException) { + // Occurs when a SCIM endpoint sends invalid date (e.g. group with empty name, user without ids...) + // No rollback required : we cannot recover. This needs to be fixed in the SCIM endpoint data + return false; + } + if (e instanceof InvalidResponseFromScimEndpointException invalidResponseFromScimEndpointException) { + return shouldRollbackBecauseOfResponse(invalidResponseFromScimEndpointException); + } + // Should not occur + throw new IllegalStateException("Unkown ScimPropagationException", e); + } + + private boolean shouldRollbackBecauseOfResponse(InvalidResponseFromScimEndpointException e) { + // If we have a response + return e.getResponse().map(r -> { + // We consider that 404 are acceptable, otherwise rollback + ArrayList acceptableStatus = Lists.newArrayList(200, 204, 404); + return !acceptableStatus.contains(r.getHttpStatus()); + }).orElse( + // Never got an answer, server was either misconfigured or unreachable + // No rollback in that case. + false + ); + } + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java new file mode 100644 index 0000000000..90d859305c --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/RollbackStrategy.java @@ -0,0 +1,22 @@ +package sh.libre.scim.core.exceptions; + +import sh.libre.scim.core.ScrimEndPointConfiguration; + +/** + * In charge of deciding, when facing a SCIM-related issue during an operation (e.g User creation), + * whether we should : + * - Log the issue and let the operation succeed in Keycloack database (potentially unsynchronising + * Keycloack with the SCIM servers) + * - Rollback the whole operation + */ +public interface RollbackStrategy { + + /** + * Indicates whether we should rollback the whole transaction because of the given exception. + * + * @param configuration The SCIM Endpoint configuration for which the exception occured + * @param e the exception that we have to handle + * @return true if transaction should be rolled back, false if we should log and continue operation + */ + boolean shouldRollback(ScrimEndPointConfiguration configuration, ScimPropagationException e); +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java new file mode 100644 index 0000000000..78973d2ad6 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java @@ -0,0 +1,43 @@ +package sh.libre.scim.core.exceptions; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import sh.libre.scim.core.ScrimEndPointConfiguration; + +/** + * In charge of dealing with SCIM exceptions by ignoring, logging or rollback transaction according to : + * - The context in which it occurs (sync, user creation...) + * - The related SCIM endpoint and its configuration + * - The thrown exception itself + */ +public class ScimExceptionHandler { + private static final Logger LOGGER = Logger.getLogger(ScimExceptionHandler.class); + + private final KeycloakSession session; + private final RollbackStrategy rollbackStrategy; + + public ScimExceptionHandler(KeycloakSession session) { + this(session, RollbackApproach.CRITICAL_ONLY_ROLLBACK); + } + + public ScimExceptionHandler(KeycloakSession session, RollbackStrategy rollbackStrategy) { + this.session = session; + this.rollbackStrategy = rollbackStrategy; + } + + /** + * Handles the given exception by loggin and/or rollback transaction. + * + * @param scimProviderConfiguration the configuration of the endpoint for which the propagation exception occured + * @param e the occuring exception + */ + public void handleException(ScrimEndPointConfiguration scimProviderConfiguration, ScimPropagationException e) { + String errorMessage = "[SCIM] Error while propagating to SCIM endpoint " + scimProviderConfiguration.getName(); + if (rollbackStrategy.shouldRollback(scimProviderConfiguration, e)) { + session.getTransactionManager().rollback(); + LOGGER.error("TRANSACTION ROLLBACK - " + errorMessage, e); + } else { + LOGGER.warn(errorMessage, e); + } + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java new file mode 100644 index 0000000000..bee5ee18fd --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/ScimPropagationException.java @@ -0,0 +1,12 @@ +package sh.libre.scim.core.exceptions; + +public abstract class ScimPropagationException extends Exception { + + protected ScimPropagationException(String message) { + super(message); + } + + protected ScimPropagationException(String message, Exception e) { + super(message, e); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java new file mode 100644 index 0000000000..e0669d59db --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopApproach.java @@ -0,0 +1,59 @@ +package sh.libre.scim.core.exceptions; + +import sh.libre.scim.core.ScrimEndPointConfiguration; + + +public enum SkipOrStopApproach implements SkipOrStopStrategy { + ALWAYS_SKIP_AND_CONTINUE { + @Override + public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) { + return false; + } + + @Override + public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) { + return false; + } + + @Override + public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) { + return false; + } + + @Override + public boolean allowInvalidEndpointConfiguration() { + return false; + } + + @Override + public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) { + return false; + } + }, + ALWAYS_STOP { + @Override + public boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration) { + return true; + } + + @Override + public boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration) { + return true; + } + + @Override + public boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration) { + return true; + } + + @Override + public boolean allowInvalidEndpointConfiguration() { + return true; + } + + @Override + public boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration) { + return true; + } + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java new file mode 100644 index 0000000000..8ad46c7ff9 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/SkipOrStopStrategy.java @@ -0,0 +1,66 @@ +package sh.libre.scim.core.exceptions; + +import sh.libre.scim.core.ScrimEndPointConfiguration; + +/** + * In charge of deciding, when facing a SCIM-related issue, whether we should : + * - log a warning, skip the problematic element and continue the rest of the operation + * - stop immediately the whole operation (typically, a synchronisation between SCIM and Keycloack) + */ +public interface SkipOrStopStrategy { + /** + * Indicates if, during a synchronisation from Keycloack to a SCIM endpoint, we should : + * - cancel the whole synchronisation if an element CRUD fail, or + * - keep on with synchronisation, allowing a partial synchronisation + * + * @param configuration the configuration of the endpoint in which the error occurred + * @return true if a partial synchronisation is allowed, + * false if we should stop the whole synchronisation at first issue + */ + boolean allowPartialSynchronizationWhenPushingToScim(ScrimEndPointConfiguration configuration); + + /** + * Indicates if, during a synchronisation from a SCIM endpoint to Keycloack, we should : + * - cancel the whole synchronisation if an element CRUD fail, or + * - keep on with synchronisation, allowing a partial synchronisation + * + * @param configuration the configuration of the endpoint in which the error occurred + * @return true if a partial synchronisation is allowed, + * false if we should interrupt the whole synchronisation at first issue + */ + boolean allowPartialSynchronizationWhenPullingFromScim(ScrimEndPointConfiguration configuration); + + + /** + * Indicates if, when we propagate a group creation or update to a SCIM endpoint and some + * of its members are not mapped to SCIM, we should allow partial group update or interrupt completely. + * + * @param configuration the configuration of the endpoint in which the error occurred + * @return true if a partial group update is allowed, + * false if we should interrupt the group update in case of any unmapped member + */ + boolean allowMissingMembersWhenPushingGroupToScim(ScrimEndPointConfiguration configuration); + + /** + * Indicates if, when facing an invalid SCIM endpoint configuration (resulting in a unreachable SCIM server), + * we should stop or ignore this configuration. + * + * @return true the invalid endpoint should be ignored, + * * false if we should interrupt the rest of the synchronisation + */ + boolean allowInvalidEndpointConfiguration(); + + /** + * Indicates if, when trying to pull User or Groups from a SCIM endpoint, + * we encounter a invalid data (e.g. group with empty name), we should : + * - Skip the invalid element pull and continue + * - Cancel the whole synchronisation + * + * @param configuration the configuration of the endpoint in which the error occurred + * @return true if we should skip the invalid data synchronisation and pursue, + * false if we should interrupt immediately the whole synchronisation + */ + boolean skipInvalidDataFromScimEndpoint(ScrimEndPointConfiguration configuration); + + +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java new file mode 100644 index 0000000000..918127ef0b --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/exceptions/UnexpectedScimDataException.java @@ -0,0 +1,7 @@ +package sh.libre.scim.core.exceptions; + +public class UnexpectedScimDataException extends ScimPropagationException { + public UnexpectedScimDataException(String message) { + super(message); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/AbstractScimService.java b/federation/scim/src/main/java/sh/libre/scim/core/service/AbstractScimService.java new file mode 100644 index 0000000000..b22b6a9a8d --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/AbstractScimService.java @@ -0,0 +1,281 @@ +package sh.libre.scim.core.service; + +import de.captaingoldfish.scim.sdk.common.resources.ResourceNode; +import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RoleMapperModel; +import org.keycloak.storage.user.SynchronizationResult; +import sh.libre.scim.core.ScrimEndPointConfiguration; +import sh.libre.scim.core.exceptions.InconsistentScimMappingException; +import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException; +import sh.libre.scim.core.exceptions.SkipOrStopStrategy; +import sh.libre.scim.core.exceptions.UnexpectedScimDataException; +import sh.libre.scim.jpa.ScimResourceDao; +import sh.libre.scim.jpa.ScimResourceMapping; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A service in charge of synchronisation (CRUD) between + * a Keykloak Role (UserModel, GroupModel) and a SCIM Resource (User,Group). + * + * @param The Keycloack Model (e.g. UserModel, GroupModel) + * @param The SCIM Resource (e.g. User, Group) + */ +public abstract class AbstractScimService implements AutoCloseable { + + private static final Logger LOGGER = Logger.getLogger(AbstractScimService.class); + + private final KeycloakSession keycloakSession; + protected final SkipOrStopStrategy skipOrStopStrategy; + private final ScrimEndPointConfiguration scimProviderConfiguration; + private final ScimResourceType type; + private final ScimClient scimClient; + + protected AbstractScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType type, SkipOrStopStrategy skipOrStopStrategy) { + this.keycloakSession = keycloakSession; + this.scimProviderConfiguration = scimProviderConfiguration; + this.type = type; + this.scimClient = ScimClient.open(scimProviderConfiguration, type); + this.skipOrStopStrategy = skipOrStopStrategy; + } + + public void create(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException { + if (isMarkedToIgnore(roleMapperModel)) { + // Silently return: resource is explicitly marked as to ignore + return; + } + // If mapping, then we are trying to recreate a user that was already created by import + KeycloakId id = getId(roleMapperModel); + if (findMappingById(id).isPresent()) { + throw new InconsistentScimMappingException("Trying to create user with id " + id + ": id already exists in Keycloak database"); + } + S scimForCreation = scimRequestBodyForCreate(roleMapperModel); + EntityOnRemoteScimId externalId = scimClient.create(id, scimForCreation); + createMapping(id, externalId); + } + + public void update(K roleMapperModel) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException { + if (isMarkedToIgnore(roleMapperModel)) { + // Silently return: resource is explicitly marked as to ignore + return; + } + KeycloakId keycloakId = getId(roleMapperModel); + EntityOnRemoteScimId entityOnRemoteScimId = findMappingById(keycloakId) + .map(ScimResourceMapping::getExternalIdAsEntityOnRemoteScimId) + .orElseThrow(() -> new InconsistentScimMappingException("Failed to find SCIM mapping for " + keycloakId)); + S scimForReplace = scimRequestBodyForUpdate(roleMapperModel, entityOnRemoteScimId); + scimClient.update(entityOnRemoteScimId, scimForReplace); + } + + protected abstract S scimRequestBodyForUpdate(K roleMapperModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException; + + public void delete(KeycloakId id) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException { + ScimResourceMapping resource = findMappingById(id) + .orElseThrow(() -> new InconsistentScimMappingException("Failed to delete resource %s, scim mapping not found: ".formatted(id))); + EntityOnRemoteScimId externalId = resource.getExternalIdAsEntityOnRemoteScimId(); + scimClient.delete(externalId); + getScimResourceDao().delete(resource); + } + + public void pushAllResourcesToScim(SynchronizationResult syncRes) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException { + LOGGER.info("[SCIM] Push resources to endpoint " + this.getConfiguration().getEndPoint()); + try (Stream resourcesStream = getResourceStream()) { + Set resources = resourcesStream.collect(Collectors.toUnmodifiableSet()); + for (K resource : resources) { + KeycloakId id = getId(resource); + pushSingleResourceToScim(syncRes, resource, id); + } + } + } + + public void pullAllResourcesFromScim(SynchronizationResult syncRes) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException { + LOGGER.info("[SCIM] Pull resources from endpoint " + this.getConfiguration().getEndPoint()); + for (S resource : scimClient.listResources()) { + pullSingleResourceFromScim(syncRes, resource); + } + } + + private void pushSingleResourceToScim(SynchronizationResult syncRes, K resource, KeycloakId id) throws InvalidResponseFromScimEndpointException, InconsistentScimMappingException { + try { + LOGGER.infof("[SCIM] Reconciling local resource %s", id); + if (shouldIgnoreForScimSynchronization(resource)) { + LOGGER.infof("[SCIM] Skip local resource %s", id); + return; + } + if (findMappingById(id).isPresent()) { + LOGGER.info("[SCIM] Replacing it"); + update(resource); + } else { + LOGGER.info("[SCIM] Creating it"); + create(resource); + } + syncRes.increaseUpdated(); + } catch (InvalidResponseFromScimEndpointException e) { + if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) { + LOGGER.warn("Error while syncing " + id + " to endpoint " + getConfiguration().getEndPoint(), e); + } else { + throw e; + } + } catch (InconsistentScimMappingException e) { + if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) { + LOGGER.warn("Inconsistent data for element " + id + " and endpoint " + getConfiguration().getEndPoint(), e); + } else { + throw e; + } + } + } + + + private void pullSingleResourceFromScim(SynchronizationResult syncRes, S resource) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException { + try { + LOGGER.infof("[SCIM] Reconciling remote resource %s", resource); + EntityOnRemoteScimId externalId = resource.getId() + .map(EntityOnRemoteScimId::new) + .orElseThrow(() -> new UnexpectedScimDataException("Remote SCIM resource doesn't have an id, cannot import it in Keycloak")); + if (validMappingAlreadyExists(externalId)) return; + + // Here no keycloak user/group matching the SCIM external id exists + // Try to match existing keycloak resource by properties (username, email, name) + Optional mapped = matchKeycloakMappingByScimProperties(resource); + if (mapped.isPresent()) { + // If found a mapped, update + LOGGER.info("[SCIM] Matched SCIM resource " + externalId + " from properties with keycloak entity " + mapped.get()); + createMapping(mapped.get(), externalId); + syncRes.increaseUpdated(); + } else { + // If not, create it locally or deleting it remotely (according to the configured Import Action) + createLocalOrDeleteRemote(syncRes, resource, externalId); + } + } catch (UnexpectedScimDataException e) { + if (skipOrStopStrategy.skipInvalidDataFromScimEndpoint(getConfiguration())) { + LOGGER.warn("[SCIM] Skipping element synchronisation because of invalid Scim Data for element " + resource.getId() + " : " + e.getMessage(), e); + } else { + throw e; + } + } catch (InconsistentScimMappingException e) { + if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) { + LOGGER.warn("[SCIM] Skipping element synchronisation because of inconsistent mapping for element " + resource.getId() + " : " + e.getMessage(), e); + } else { + throw e; + } + } catch (InvalidResponseFromScimEndpointException e) { + // Can only occur in case of a DELETE_REMOTE conflict action + if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) { + LOGGER.warn("[SCIM] Could not delete SCIM resource " + resource.getId() + " during synchronisation", e); + } else { + throw e; + } + } + + } + + private boolean validMappingAlreadyExists(EntityOnRemoteScimId externalId) { + Optional optionalMapping = getScimResourceDao().findByExternalId(externalId, type); + // If an existing mapping exists, delete potential dangling references + if (optionalMapping.isPresent()) { + ScimResourceMapping mapping = optionalMapping.get(); + if (entityExists(mapping.getIdAsKeycloakId())) { + LOGGER.info("[SCIM] Valid mapping found, skipping"); + return true; + } else { + LOGGER.info("[SCIM] Delete a dangling mapping"); + getScimResourceDao().delete(mapping); + } + } + return false; + } + + private void createLocalOrDeleteRemote(SynchronizationResult syncRes, S resource, EntityOnRemoteScimId externalId) throws UnexpectedScimDataException, InconsistentScimMappingException, InvalidResponseFromScimEndpointException { + switch (scimProviderConfiguration.getImportAction()) { + case CREATE_LOCAL -> { + LOGGER.info("[SCIM] Create local resource for SCIM resource " + externalId); + KeycloakId id = createEntity(resource); + createMapping(id, externalId); + syncRes.increaseAdded(); + } + case DELETE_REMOTE -> { + LOGGER.info("[SCIM] Delete remote resource " + externalId); + scimClient.delete(externalId); + } + case NOTHING -> LOGGER.info("[SCIM] Import action set to NOTHING"); + } + } + + + protected abstract S scimRequestBodyForCreate(K roleMapperModel) throws InconsistentScimMappingException; + + protected abstract KeycloakId getId(K roleMapperModel); + + protected abstract boolean isMarkedToIgnore(K roleMapperModel); + + private void createMapping(KeycloakId keycloakId, EntityOnRemoteScimId externalId) { + getScimResourceDao().create(keycloakId, externalId, type); + } + + protected ScimResourceDao getScimResourceDao() { + return ScimResourceDao.newInstance(getKeycloakSession(), scimProviderConfiguration.getId()); + } + + private Optional findMappingById(KeycloakId keycloakId) { + return getScimResourceDao().findById(keycloakId, type); + } + + private KeycloakSession getKeycloakSession() { + return keycloakSession; + } + + + protected abstract boolean shouldIgnoreForScimSynchronization(K resource); + + protected abstract Stream getResourceStream(); + + protected abstract KeycloakId createEntity(S resource) throws UnexpectedScimDataException, InconsistentScimMappingException; + + protected abstract Optional matchKeycloakMappingByScimProperties(S resource) throws InconsistentScimMappingException; + + protected abstract boolean entityExists(KeycloakId keycloakId); + + public void sync(SynchronizationResult syncRes) throws InconsistentScimMappingException, InvalidResponseFromScimEndpointException, UnexpectedScimDataException { + if (this.scimProviderConfiguration.isPullFromScimSynchronisationActivated()) { + this.pullAllResourcesFromScim(syncRes); + } + if (this.scimProviderConfiguration.isPushToScimSynchronisationActivated()) { + this.pushAllResourcesToScim(syncRes); + } + } + + protected Meta newMetaLocation(EntityOnRemoteScimId externalId) { + Meta meta = new Meta(); + URI uri = getUri(type, externalId); + meta.setLocation(uri.toString()); + return meta; + } + + protected URI getUri(ScimResourceType type, EntityOnRemoteScimId externalId) { + try { + return new URI("%s/%s".formatted(type.getEndpoint(), externalId.asString())); + } catch (URISyntaxException e) { + throw new IllegalStateException("should never occur: can not format URI for type %s and id %s".formatted(type, externalId), e); + } + } + + protected KeycloakDao getKeycloakDao() { + return new KeycloakDao(getKeycloakSession()); + } + + @Override + public void close() { + scimClient.close(); + } + + public ScrimEndPointConfiguration getConfiguration() { + return scimProviderConfiguration; + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java b/federation/scim/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java new file mode 100644 index 0000000000..df96a12323 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/EntityOnRemoteScimId.java @@ -0,0 +1,6 @@ +package sh.libre.scim.core.service; + +public record EntityOnRemoteScimId( + String asString +) { +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/GroupScimService.java b/federation/scim/src/main/java/sh/libre/scim/core/service/GroupScimService.java new file mode 100644 index 0000000000..bd09f3e9b2 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/GroupScimService.java @@ -0,0 +1,131 @@ +package sh.libre.scim.core.service; + +import de.captaingoldfish.scim.sdk.common.resources.Group; +import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; +import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.jboss.logging.Logger; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserModel; +import sh.libre.scim.core.ScrimEndPointConfiguration; +import sh.libre.scim.core.exceptions.InconsistentScimMappingException; +import sh.libre.scim.core.exceptions.SkipOrStopStrategy; +import sh.libre.scim.core.exceptions.UnexpectedScimDataException; +import sh.libre.scim.jpa.ScimResourceMapping; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; + +public class GroupScimService extends AbstractScimService { + private static final Logger LOGGER = Logger.getLogger(GroupScimService.class); + + public GroupScimService(KeycloakSession keycloakSession, ScrimEndPointConfiguration scimProviderConfiguration, SkipOrStopStrategy skipOrStopStrategy) { + super(keycloakSession, scimProviderConfiguration, ScimResourceType.GROUP, skipOrStopStrategy); + } + + @Override + protected Stream getResourceStream() { + return getKeycloakDao().getGroupsStream(); + } + + @Override + protected boolean entityExists(KeycloakId keycloakId) { + return getKeycloakDao().groupExists(keycloakId); + } + + @Override + protected Optional matchKeycloakMappingByScimProperties(Group resource) { + Set names = new TreeSet<>(); + resource.getId().ifPresent(names::add); + resource.getDisplayName().ifPresent(names::add); + try (Stream groupsStream = getKeycloakDao().getGroupsStream()) { + Optional group = groupsStream + .filter(groupModel -> names.contains(groupModel.getName())) + .findFirst(); + return group + .map(GroupModel::getId) + .map(KeycloakId::new); + } + } + + @Override + protected KeycloakId createEntity(Group resource) throws UnexpectedScimDataException, InconsistentScimMappingException { + String displayName = resource.getDisplayName() + .filter(StringUtils::isNotBlank) + .orElseThrow(() -> new UnexpectedScimDataException("Remote Scim group has empty name, can't create. Resource id = %s".formatted(resource.getId()))); + GroupModel group = getKeycloakDao().createGroup(displayName); + List groupMembers = resource.getMembers(); + if (CollectionUtils.isNotEmpty(groupMembers)) { + for (Member groupMember : groupMembers) { + EntityOnRemoteScimId externalId = groupMember.getValue() + .map(EntityOnRemoteScimId::new) + .orElseThrow(() -> new UnexpectedScimDataException("can't create group member for group '%s' without id: ".formatted(displayName) + resource)); + KeycloakId userId = getScimResourceDao().findUserByExternalId(externalId) + .map(ScimResourceMapping::getIdAsKeycloakId) + .orElseThrow(() -> new InconsistentScimMappingException("can't find mapping for group member %s".formatted(externalId))); + UserModel userModel = getKeycloakDao().getUserById(userId); + userModel.joinGroup(group); + } + } + return new KeycloakId(group.getId()); + } + + @Override + protected boolean isMarkedToIgnore(GroupModel groupModel) { + return BooleanUtils.TRUE.equals(groupModel.getFirstAttribute("scim-skip")); + } + + @Override + protected KeycloakId getId(GroupModel groupModel) { + return new KeycloakId(groupModel.getId()); + } + + @Override + protected Group scimRequestBodyForCreate(GroupModel groupModel) throws InconsistentScimMappingException { + Set members = getKeycloakDao().getGroupMembers(groupModel); + Group group = new Group(); + group.setExternalId(groupModel.getId()); + group.setDisplayName(groupModel.getName()); + for (KeycloakId member : members) { + Member groupMember = new Member(); + Optional optionalGroupMemberMapping = getScimResourceDao().findUserById(member); + if (optionalGroupMemberMapping.isPresent()) { + ScimResourceMapping groupMemberMapping = optionalGroupMemberMapping.get(); + EntityOnRemoteScimId externalIdAsEntityOnRemoteScimId = groupMemberMapping.getExternalIdAsEntityOnRemoteScimId(); + groupMember.setValue(externalIdAsEntityOnRemoteScimId.asString()); + URI ref = getUri(ScimResourceType.USER, externalIdAsEntityOnRemoteScimId); + groupMember.setRef(ref.toString()); + group.addMember(groupMember); + } else { + String message = "Unmapped member " + member + " for group " + groupModel.getId(); + if (skipOrStopStrategy.allowMissingMembersWhenPushingGroupToScim(this.getConfiguration())) { + LOGGER.warn(message); + } else { + throw new InconsistentScimMappingException(message); + } + } + } + return group; + } + + @Override + protected Group scimRequestBodyForUpdate(GroupModel groupModel, EntityOnRemoteScimId externalId) throws InconsistentScimMappingException { + Group group = scimRequestBodyForCreate(groupModel); + group.setId(externalId.asString()); + Meta meta = newMetaLocation(externalId); + group.setMeta(meta); + return group; + } + + @Override + protected boolean shouldIgnoreForScimSynchronization(GroupModel resource) { + return false; + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakDao.java b/federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakDao.java new file mode 100644 index 0000000000..f4c406c351 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakDao.java @@ -0,0 +1,81 @@ +package sh.libre.scim.core.service; + +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class KeycloakDao { + + private final KeycloakSession keycloakSession; + + public KeycloakDao(KeycloakSession keycloakSession) { + this.keycloakSession = keycloakSession; + } + + private KeycloakSession getKeycloakSession() { + return keycloakSession; + } + + private RealmModel getRealm() { + return getKeycloakSession().getContext().getRealm(); + } + + public boolean groupExists(KeycloakId groupId) { + GroupModel group = getKeycloakSession().groups().getGroupById(getRealm(), groupId.asString()); + return group != null; + } + + public boolean userExists(KeycloakId userId) { + UserModel user = getUserById(userId); + return user != null; + } + + public UserModel getUserById(KeycloakId userId) { + return getKeycloakSession().users().getUserById(getRealm(), userId.asString()); + } + + public GroupModel getGroupById(KeycloakId groupId) { + return getKeycloakSession().groups().getGroupById(getRealm(), groupId.asString()); + } + + + public Stream getGroupsStream() { + return getKeycloakSession().groups().getGroupsStream(getRealm()); + } + + public GroupModel createGroup(String displayName) { + return getKeycloakSession().groups().createGroup(getRealm(), displayName); + } + + public Set getGroupMembers(GroupModel groupModel) { + return getKeycloakSession().users() + .getGroupMembersStream(getRealm(), groupModel) + .map(UserModel::getId) + .map(KeycloakId::new) + .collect(Collectors.toSet()); + } + + public Stream getUsersStream() { + return getKeycloakSession().users().searchForUserStream(getRealm(), Collections.emptyMap()); + } + + public UserModel getUserByUsername(String username) { + return getKeycloakSession().users().getUserByUsername(getRealm(), username); + } + + public UserModel getUserByEmail(String email) { + return getKeycloakSession().users().getUserByEmail(getRealm(), email); + } + + public UserModel addUser(String username) { + return getKeycloakSession().users().addUser(getRealm(), username); + } + + +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakId.java b/federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakId.java new file mode 100644 index 0000000000..04bad470b3 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/KeycloakId.java @@ -0,0 +1,7 @@ +package sh.libre.scim.core.service; + +public record KeycloakId( + String asString +) { + +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/ScimClient.java b/federation/scim/src/main/java/sh/libre/scim/core/service/ScimClient.java new file mode 100644 index 0000000000..de3b4d7c0f --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/ScimClient.java @@ -0,0 +1,155 @@ +package sh.libre.scim.core.service; + +import com.google.common.net.HttpHeaders; +import de.captaingoldfish.scim.sdk.client.ScimClientConfig; +import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder; +import de.captaingoldfish.scim.sdk.client.response.ServerResponse; +import de.captaingoldfish.scim.sdk.common.resources.ResourceNode; +import de.captaingoldfish.scim.sdk.common.response.ListResponse; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import io.github.resilience4j.retry.RetryRegistry; +import jakarta.ws.rs.ProcessingException; +import org.jboss.logging.Logger; +import sh.libre.scim.core.ScrimEndPointConfiguration; +import sh.libre.scim.core.exceptions.InvalidResponseFromScimEndpointException; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ScimClient implements AutoCloseable { + private static final Logger LOGGER = Logger.getLogger(ScimClient.class); + + private final RetryRegistry retryRegistry; + + private final ScimRequestBuilder scimRequestBuilder; + + private final ScimResourceType scimResourceType; + private final boolean logAllRequests; + + private ScimClient(ScimRequestBuilder scimRequestBuilder, ScimResourceType scimResourceType, boolean detailedLogs) { + this.scimRequestBuilder = scimRequestBuilder; + this.scimResourceType = scimResourceType; + RetryConfig retryConfig = RetryConfig.custom() + .maxAttempts(10) + .intervalFunction(IntervalFunction.ofExponentialBackoff()) + .retryExceptions(ProcessingException.class) + .build(); + retryRegistry = RetryRegistry.of(retryConfig); + this.logAllRequests = detailedLogs; + } + + public static ScimClient open(ScrimEndPointConfiguration scimProviderConfiguration, ScimResourceType scimResourceType) { + String scimApplicationBaseUrl = scimProviderConfiguration.getEndPoint(); + Map httpHeaders = new HashMap<>(); + httpHeaders.put(HttpHeaders.AUTHORIZATION, scimProviderConfiguration.getAuthorizationHeaderValue()); + httpHeaders.put(HttpHeaders.CONTENT_TYPE, scimProviderConfiguration.getContentType()); + ScimClientConfig scimClientConfig = ScimClientConfig.builder() + .httpHeaders(httpHeaders) + .connectTimeout(5) + .requestTimeout(5) + .socketTimeout(5) + .build(); + ScimRequestBuilder scimRequestBuilder = + new ScimRequestBuilder( + scimApplicationBaseUrl, + scimClientConfig + ); + return new ScimClient<>(scimRequestBuilder, scimResourceType, scimProviderConfiguration.isLogAllScimRequests()); + } + + public EntityOnRemoteScimId create(KeycloakId id, S scimForCreation) throws InvalidResponseFromScimEndpointException { + Optional scimForCreationId = scimForCreation.getId(); + if (scimForCreationId.isPresent()) { + throw new IllegalArgumentException( + "User to create should never have an existing id: %s %s".formatted(id, scimForCreationId.get()) + ); + } + try { + Retry retry = retryRegistry.retry("create-%s".formatted(id.asString())); + if (logAllRequests) { + LOGGER.info("[SCIM] Sending CREATE " + scimForCreation.toPrettyString() + "\n to " + getScimEndpoint()); + } + ServerResponse response = retry.executeSupplier(() -> scimRequestBuilder + .create(getResourceClass(), getScimEndpoint()) + .setResource(scimForCreation) + .sendRequest() + ); + checkResponseIsSuccess(response); + S resource = response.getResource(); + return resource.getId() + .map(EntityOnRemoteScimId::new) + .orElseThrow(() -> new InvalidResponseFromScimEndpointException(response, "Created SCIM resource does not have id")); + + } catch (Exception e) { + LOGGER.warn(e); + throw new InvalidResponseFromScimEndpointException("Exception while retrying create " + e.getMessage(), e); + } + } + + private void checkResponseIsSuccess(ServerResponse response) throws InvalidResponseFromScimEndpointException { + if (logAllRequests) { + LOGGER.info("[SCIM] Server response " + response.getHttpStatus() + "\n" + response.getResponseBody()); + } + if (!response.isSuccess()) { + throw new InvalidResponseFromScimEndpointException(response, "Server answered with status " + response.getResponseBody() + ": " + response.getResponseBody()); + } + } + + private String getScimEndpoint() { + return scimResourceType.getEndpoint(); + } + + private Class getResourceClass() { + return scimResourceType.getResourceClass(); + } + + public void update(EntityOnRemoteScimId externalId, S scimForReplace) throws InvalidResponseFromScimEndpointException { + Retry retry = retryRegistry.retry("replace-%s".formatted(externalId.asString())); + try { + if (logAllRequests) { + LOGGER.info("[SCIM] Sending UPDATE " + scimForReplace.toPrettyString() + "\n to " + getScimEndpoint()); + } + ServerResponse response = retry.executeSupplier(() -> scimRequestBuilder + .update(getResourceClass(), getScimEndpoint(), externalId.asString()) + .setResource(scimForReplace) + .sendRequest() + ); + checkResponseIsSuccess(response); + } catch (Exception e) { + LOGGER.warn(e); + throw new InvalidResponseFromScimEndpointException("Exception while retrying update " + e.getMessage(), e); + } + } + + public void delete(EntityOnRemoteScimId externalId) throws InvalidResponseFromScimEndpointException { + Retry retry = retryRegistry.retry("delete-%s".formatted(externalId.asString())); + if (logAllRequests) { + LOGGER.info("[SCIM] Sending DELETE to " + getScimEndpoint()); + } + try { + ServerResponse response = retry.executeSupplier(() -> scimRequestBuilder + .delete(getResourceClass(), getScimEndpoint(), externalId.asString()) + .sendRequest() + ); + checkResponseIsSuccess(response); + } catch (Exception e) { + LOGGER.warn(e); + throw new InvalidResponseFromScimEndpointException("Exception while retrying delete " + e.getMessage(), e); + } + } + + @Override + public void close() { + scimRequestBuilder.close(); + } + + public List listResources() { + ServerResponse> response = scimRequestBuilder.list(getResourceClass(), getScimEndpoint()).get().sendRequest(); + ListResponse resourceTypeListResponse = response.getResource(); + return resourceTypeListResponse.getListedResources(); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/ScimResourceType.java b/federation/scim/src/main/java/sh/libre/scim/core/service/ScimResourceType.java new file mode 100644 index 0000000000..b90845b6cf --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/ScimResourceType.java @@ -0,0 +1,29 @@ +package sh.libre.scim.core.service; + +import de.captaingoldfish.scim.sdk.common.resources.Group; +import de.captaingoldfish.scim.sdk.common.resources.ResourceNode; +import de.captaingoldfish.scim.sdk.common.resources.User; + +public enum ScimResourceType { + + USER("/Users", User.class), + + GROUP("/Groups", Group.class); + + private final String endpoint; + + private final Class resourceClass; + + ScimResourceType(String endpoint, Class resourceClass) { + this.endpoint = endpoint; + this.resourceClass = resourceClass; + } + + public String getEndpoint() { + return endpoint; + } + + public Class getResourceClass() { + return (Class) resourceClass; + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/core/service/UserScimService.java b/federation/scim/src/main/java/sh/libre/scim/core/service/UserScimService.java new file mode 100644 index 0000000000..c0262f3d4a --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/core/service/UserScimService.java @@ -0,0 +1,145 @@ +package sh.libre.scim.core.service; + +import de.captaingoldfish.scim.sdk.common.resources.User; +import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; +import de.captaingoldfish.scim.sdk.common.resources.complex.Name; +import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Email; +import de.captaingoldfish.scim.sdk.common.resources.multicomplex.MultiComplexNode; +import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RoleMapperModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import sh.libre.scim.core.ScrimEndPointConfiguration; +import sh.libre.scim.core.exceptions.InconsistentScimMappingException; +import sh.libre.scim.core.exceptions.SkipOrStopStrategy; +import sh.libre.scim.core.exceptions.UnexpectedScimDataException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +public class UserScimService extends AbstractScimService { + private static final Logger LOGGER = Logger.getLogger(UserScimService.class); + + public UserScimService( + KeycloakSession keycloakSession, + ScrimEndPointConfiguration scimProviderConfiguration, + SkipOrStopStrategy skipOrStopStrategy) { + super(keycloakSession, scimProviderConfiguration, ScimResourceType.USER, skipOrStopStrategy); + } + + @Override + protected Stream getResourceStream() { + return getKeycloakDao().getUsersStream(); + } + + @Override + protected boolean entityExists(KeycloakId keycloakId) { + return getKeycloakDao().userExists(keycloakId); + } + + @Override + protected Optional matchKeycloakMappingByScimProperties(User resource) throws InconsistentScimMappingException { + Optional matchedByUsername = resource.getUserName() + .map(getKeycloakDao()::getUserByUsername) + .map(this::getId); + Optional matchedByEmail = resource.getEmails().stream() + .findFirst() + .flatMap(MultiComplexNode::getValue) + .map(getKeycloakDao()::getUserByEmail) + .map(this::getId); + if (matchedByUsername.isPresent() + && matchedByEmail.isPresent() + && !matchedByUsername.equals(matchedByEmail)) { + String inconstencyErrorMessage = "Found 2 possible users for remote user " + matchedByUsername.get() + " - " + matchedByEmail.get(); + LOGGER.warn(inconstencyErrorMessage); + throw new InconsistentScimMappingException(inconstencyErrorMessage); + } + if (matchedByUsername.isPresent()) { + return matchedByUsername; + } + return matchedByEmail; + } + + @Override + protected KeycloakId createEntity(User resource) throws UnexpectedScimDataException { + String username = resource.getUserName() + .filter(StringUtils::isNotBlank) + .orElseThrow(() -> new UnexpectedScimDataException("Remote Scim user has empty username, can't create. Resource id = %s".formatted(resource.getId()))); + UserModel user = getKeycloakDao().addUser(username); + resource.getEmails().stream() + .findFirst() + .flatMap(MultiComplexNode::getValue) + .ifPresent(user::setEmail); + boolean userEnabled = resource.isActive().orElse(false); + user.setEnabled(userEnabled); + return new KeycloakId(user.getId()); + } + + @Override + protected boolean isMarkedToIgnore(UserModel userModel) { + return BooleanUtils.TRUE.equals(userModel.getFirstAttribute("scim-skip")); + } + + @Override + protected KeycloakId getId(UserModel userModel) { + return new KeycloakId(userModel.getId()); + } + + @Override + protected User scimRequestBodyForCreate(UserModel roleMapperModel) { + String firstAndLastName = String.format("%s %s", + StringUtils.defaultString(roleMapperModel.getFirstName()), + StringUtils.defaultString(roleMapperModel.getLastName())).trim(); + String displayName = Objects.toString(firstAndLastName, roleMapperModel.getUsername()); + Stream groupRoleModels = roleMapperModel.getGroupsStream().flatMap(RoleMapperModel::getRoleMappingsStream); + Stream roleModels = roleMapperModel.getRoleMappingsStream(); + Stream allRoleModels = Stream.concat(groupRoleModels, roleModels); + List roles = allRoleModels + .filter(r -> BooleanUtils.TRUE.equals(r.getFirstAttribute("scim"))) + .map(RoleModel::getName) + .map(roleName -> { + PersonRole personRole = new PersonRole(); + personRole.setValue(roleName); + return personRole; + }) + .toList(); + User user = new User(); + user.setRoles(roles); + user.setExternalId(roleMapperModel.getId()); + user.setUserName(roleMapperModel.getUsername()); + user.setDisplayName(displayName); + Name name = new Name(); + name.setFamilyName(roleMapperModel.getLastName()); + name.setGivenName(roleMapperModel.getFirstName()); + user.setName(name); + List emails = new ArrayList<>(); + if (roleMapperModel.getEmail() != null) { + emails.add( + Email.builder().value(roleMapperModel.getEmail()).build()); + } + user.setEmails(emails); + user.setActive(roleMapperModel.isEnabled()); + return user; + } + + @Override + protected User scimRequestBodyForUpdate(UserModel userModel, EntityOnRemoteScimId externalId) { + User user = scimRequestBodyForCreate(userModel); + user.setId(externalId.asString()); + Meta meta = newMetaLocation(externalId); + user.setMeta(meta); + return user; + } + + @Override + protected boolean shouldIgnoreForScimSynchronization(UserModel userModel) { + return "admin".equals(userModel.getUsername()); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java b/federation/scim/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java new file mode 100644 index 0000000000..4c49f74f67 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/event/ScimBackgroundGroupMembershipUpdater.java @@ -0,0 +1,74 @@ +package sh.libre.scim.event; + +import org.jboss.logging.Logger; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.timer.TimerProvider; +import sh.libre.scim.core.ScimDispatcher; + +import java.time.Duration; + +/** + * In charge of making background checks and sent + * UPDATE requests from group for which membership information has changed. + *

+ * This is required to avoid immediate group membership updates which could cause + * to incorrect group members list in case of concurrent group membership changes. + */ +public class ScimBackgroundGroupMembershipUpdater { + public static final String GROUP_DIRTY_SINCE_ATTRIBUTE_NAME = "scim-dirty-since"; + + private static final Logger LOGGER = Logger.getLogger(ScimBackgroundGroupMembershipUpdater.class); + // Update check loop will run every time this delay has passed + private static final long UPDATE_CHECK_DELAY_MS = 2000; + // If a group is marked dirty since less that this debounce delay, wait for the next update check loop + private static final long DEBOUNCE_DELAY_MS = 1200; + private final KeycloakSessionFactory sessionFactory; + + public ScimBackgroundGroupMembershipUpdater(KeycloakSessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + public void startBackgroundUpdates() { + // Every UPDATE_CHECK_DELAY_MS, check for dirty groups and send updates if required + try (KeycloakSession keycloakSession = sessionFactory.create()) { + TimerProvider timer = keycloakSession.getProvider(TimerProvider.class); + timer.scheduleTask(taskSession -> { + for (RealmModel realm : taskSession.realms().getRealmsStream().toList()) { + dispatchDirtyGroupsUpdates(realm); + } + }, Duration.ofMillis(UPDATE_CHECK_DELAY_MS).toMillis(), "scim-background"); + } + } + + private void dispatchDirtyGroupsUpdates(RealmModel realm) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { + session.getContext().setRealm(realm); + ScimDispatcher dispatcher = new ScimDispatcher(session); + // Identify groups marked as dirty by the ScimEventListenerProvider + for (GroupModel group : session.groups().getGroupsStream(realm) + .filter(this::isDirtyGroup).toList()) { + LOGGER.infof("[SCIM] Group %s is dirty, dispatch an update", group.getName()); + // If dirty : dispatch a group update to all clients and mark it clean + dispatcher.dispatchGroupModificationToAll(client -> client.update(group)); + group.removeAttribute(GROUP_DIRTY_SINCE_ATTRIBUTE_NAME); + } + dispatcher.close(); + }); + } + + private boolean isDirtyGroup(GroupModel g) { + String groupDirtySinceAttribute = g.getFirstAttribute(GROUP_DIRTY_SINCE_ATTRIBUTE_NAME); + try { + long groupDirtySince = Long.parseLong(groupDirtySinceAttribute); + // Must be dirty for more than DEBOUNCE_DELAY_MS + // (otherwise update will be dispatched in next scheduled loop) + return System.currentTimeMillis() - groupDirtySince > DEBOUNCE_DELAY_MS; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java b/federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java new file mode 100644 index 0000000000..6c49522982 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java @@ -0,0 +1,252 @@ +package sh.libre.scim.event; + +import org.jboss.logging.Logger; +import org.keycloak.common.Profile; +import org.keycloak.component.ComponentModel; +import org.keycloak.events.Event; +import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.EventType; +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.events.admin.OperationType; +import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserModel; +import sh.libre.scim.core.ScimDispatcher; +import sh.libre.scim.core.ScimEndpointConfigurationStorageProviderFactory; +import sh.libre.scim.core.service.KeycloakDao; +import sh.libre.scim.core.service.KeycloakId; +import sh.libre.scim.core.service.ScimResourceType; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * An Event listener reacting to Keycloak models modification + * (e.g. User creation, Group deletion, membership modifications, endpoint configuration change...) + * by propagating it to all registered Scim endpoints. + */ +public class ScimEventListenerProvider implements EventListenerProvider { + + private static final Logger LOGGER = Logger.getLogger(ScimEventListenerProvider.class); + + private final ScimDispatcher dispatcher; + + private final KeycloakSession session; + + private final KeycloakDao keycloakDao; + + private final Map listenedEventPathPatterns = Map.of( + ResourceType.USER, Pattern.compile("users/(.+)"), + ResourceType.GROUP, Pattern.compile("groups/([\\w-]+)(/children)?"), + ResourceType.GROUP_MEMBERSHIP, Pattern.compile("users/(.+)/groups/(.+)"), + ResourceType.REALM_ROLE_MAPPING, Pattern.compile("^(.+)/(.+)/role-mappings"), + ResourceType.COMPONENT, Pattern.compile("components/(.+)") + ); + + public ScimEventListenerProvider(KeycloakSession session) { + this.session = session; + this.keycloakDao = new KeycloakDao(session); + this.dispatcher = new ScimDispatcher(session); + } + + @Override + public void onEvent(Event event) { + if (!Profile.isFeatureEnabled(Profile.Feature.SCIM)) { + // React to User-related event : creation, deletion, update + EventType eventType = event.getType(); + KeycloakId eventUserId = new KeycloakId(event.getUserId()); + switch (eventType) { + case REGISTER -> { + LOGGER.infof("[SCIM] Propagate User Registration - %s", eventUserId); + UserModel user = getUser(eventUserId); + dispatcher.dispatchUserModificationToAll(client -> client.create(user)); + } + case UPDATE_EMAIL, UPDATE_PROFILE -> { + LOGGER.infof("[SCIM] Propagate User %s - %s", eventType, eventUserId); + UserModel user = getUser(eventUserId); + dispatcher.dispatchUserModificationToAll(client -> client.update(user)); + } + case DELETE_ACCOUNT -> { + LOGGER.infof("[SCIM] Propagate User deletion - %s", eventUserId); + dispatcher.dispatchUserModificationToAll(client -> client.delete(eventUserId)); + } + default -> { + // No other event has to be propagated to Scim endpoints + } + } + } + } + + + @Override + public void onEvent(AdminEvent event, boolean includeRepresentation) { + if (!Profile.isFeatureEnabled(Profile.Feature.SCIM)) { + // Step 1: check if event is relevant for propagation through SCIM + Pattern pattern = listenedEventPathPatterns.get(event.getResourceType()); + if (pattern == null) + return; + Matcher matcher = pattern.matcher(event.getResourcePath()); + if (!matcher.find()) + return; + + + // Step 2: propagate event (if needed) according to its resource type + switch (event.getResourceType()) { + case USER -> { + KeycloakId userId = new KeycloakId(matcher.group(1)); + handleUserEvent(event, userId); + } + case GROUP -> { + KeycloakId groupId = new KeycloakId(matcher.group(1)); + handleGroupEvent(event, groupId); + } + case GROUP_MEMBERSHIP -> { + KeycloakId userId = new KeycloakId(matcher.group(1)); + KeycloakId groupId = new KeycloakId(matcher.group(2)); + handleGroupMemberShipEvent(event, userId, groupId); + } + case REALM_ROLE_MAPPING -> { + String rawResourceType = matcher.group(1); + ScimResourceType type = switch (rawResourceType) { + case "users" -> ScimResourceType.USER; + case "groups" -> ScimResourceType.GROUP; + default -> throw new IllegalArgumentException("Unsupported resource type: " + rawResourceType); + }; + KeycloakId id = new KeycloakId(matcher.group(2)); + handleRoleMappingEvent(event, type, id); + } + case COMPONENT -> { + String id = matcher.group(1); + handleScimEndpointConfigurationEvent(event, id); + + } + default -> { + // No other resource modification has to be propagated to Scim endpoints + } + } + } + } + + + private void handleUserEvent(AdminEvent userEvent, KeycloakId userId) { + LOGGER.infof("[SCIM] Propagate User %s - %s", userEvent.getOperationType(), userId); + switch (userEvent.getOperationType()) { + case CREATE -> { + UserModel user = getUser(userId); + dispatcher.dispatchUserModificationToAll(client -> client.create(user)); + user.getGroupsStream().forEach(group -> + dispatcher.dispatchGroupModificationToAll(client -> client.update(group) + )); + } + case UPDATE -> { + UserModel user = getUser(userId); + dispatcher.dispatchUserModificationToAll(client -> client.update(user)); + } + case DELETE -> dispatcher.dispatchUserModificationToAll(client -> client.delete(userId)); + default -> { + // ACTION userEvent are not relevant, nothing to do + } + } + } + + /** + * Propagating the given group-related event to Scim endpoints. + * + * @param event the event to propagate + * @param groupId event target's id + */ + private void handleGroupEvent(AdminEvent event, KeycloakId groupId) { + LOGGER.infof("[SCIM] Propagate Group %s - %s", event.getOperationType(), groupId); + switch (event.getOperationType()) { + case CREATE -> { + GroupModel group = getGroup(groupId); + dispatcher.dispatchGroupModificationToAll(client -> client.create(group)); + } + case UPDATE -> { + GroupModel group = getGroup(groupId); + dispatcher.dispatchGroupModificationToAll(client -> client.update(group)); + } + case DELETE -> dispatcher.dispatchGroupModificationToAll(client -> client.delete(groupId)); + default -> { + // ACTION event are not relevant, nothing to do + } + } + } + + private void handleGroupMemberShipEvent(AdminEvent groupMemberShipEvent, KeycloakId userId, KeycloakId groupId) { + LOGGER.infof("[SCIM] Propagate GroupMemberShip %s - User %s Group %s", groupMemberShipEvent.getOperationType(), userId, groupId); + // Step 1: update USER immediately + GroupModel group = getGroup(groupId); + UserModel user = getUser(userId); + dispatcher.dispatchUserModificationToAll(client -> client.update(user)); + + // Step 2: delayed GROUP update : + // if several users are added to the group simultaneously in different Keycloack sessions + // update the group in the context of the current session may not reflect those other changes + // We trigger a delayed update by setting an attribute on the group (that will be handled by ScimBackgroundGroupMembershipUpdaters) + group.setSingleAttribute(ScimBackgroundGroupMembershipUpdater.GROUP_DIRTY_SINCE_ATTRIBUTE_NAME, "" + System.currentTimeMillis()); + } + + private void handleRoleMappingEvent(AdminEvent roleMappingEvent, ScimResourceType type, KeycloakId id) { + LOGGER.infof("[SCIM] Propagate RoleMapping %s - %s %s", roleMappingEvent.getOperationType(), type, id); + switch (type) { + case USER -> { + UserModel user = getUser(id); + dispatcher.dispatchUserModificationToAll(client -> client.update(user)); + } + case GROUP -> { + GroupModel group = getGroup(id); + session.users() + .getGroupMembersStream(session.getContext().getRealm(), group) + .forEach(user -> + dispatcher.dispatchUserModificationToAll(client -> client.update(user) + )); + } + default -> { + // No other type is relevant for propagation + } + } + } + + private void handleScimEndpointConfigurationEvent(AdminEvent event, String id) { + // In case of a component deletion + if (event.getOperationType() == OperationType.DELETE) { + // Check if it was a Scim endpoint configuration, and forward deletion if so + Stream scimEndpointConfigurationsWithDeletedId = session.getContext().getRealm().getComponentsStream() + .filter(m -> ScimEndpointConfigurationStorageProviderFactory.ID.equals(m.getProviderId()) + && id.equals(m.getId())); + if (scimEndpointConfigurationsWithDeletedId.iterator().hasNext()) { + LOGGER.infof("[SCIM] SCIM Endpoint configuration DELETE - %s ", id); + dispatcher.refreshActiveScimEndpoints(); + } + } else { + // In case of CREATE or UPDATE, we can directly use the string representation + // to check if it defines a SCIM endpoint (faster) + if (event.getRepresentation() != null + && event.getRepresentation().contains("\"providerId\":\"scim\"")) { + LOGGER.infof("[SCIM] SCIM Endpoint configuration CREATE - %s ", id); + dispatcher.refreshActiveScimEndpoints(); + } + } + + } + + + private UserModel getUser(KeycloakId id) { + return keycloakDao.getUserById(id); + } + + private GroupModel getGroup(KeycloakId id) { + return keycloakDao.getGroupById(id); + } + + @Override + public void close() { + dispatcher.close(); + } + + +} diff --git a/federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java b/federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java new file mode 100644 index 0000000000..c7b437a287 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java @@ -0,0 +1,36 @@ +package sh.libre.scim.event; + +import org.keycloak.Config.Scope; +import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.EventListenerProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +public class ScimEventListenerProviderFactory implements EventListenerProviderFactory { + + @Override + public EventListenerProvider create(KeycloakSession session) { + return new ScimEventListenerProvider(session); + } + + @Override + public String getId() { + return "scim"; + } + + @Override + public void init(Scope config) { + // Nothing to initialize + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // Nothing to initialize + } + + @Override + public void close() { + // Nothing to close + } + +} diff --git a/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java new file mode 100644 index 0000000000..4deec373b9 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceDao.java @@ -0,0 +1,96 @@ +package sh.libre.scim.jpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.TypedQuery; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.models.KeycloakSession; +import sh.libre.scim.core.service.EntityOnRemoteScimId; +import sh.libre.scim.core.service.KeycloakId; +import sh.libre.scim.core.service.ScimResourceType; + +import java.util.Optional; + +public class ScimResourceDao { + + private final String realmId; + + private final String componentId; + + private final EntityManager entityManager; + + private ScimResourceDao(String realmId, String componentId, EntityManager entityManager) { + this.realmId = realmId; + this.componentId = componentId; + this.entityManager = entityManager; + } + + public static ScimResourceDao newInstance(KeycloakSession keycloakSession, String componentId) { + String realmId = keycloakSession.getContext().getRealm().getId(); + EntityManager entityManager = keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager(); + return new ScimResourceDao(realmId, componentId, entityManager); + } + + private EntityManager getEntityManager() { + return entityManager; + } + + private String getRealmId() { + return realmId; + } + + private String getComponentId() { + return componentId; + } + + public void create(KeycloakId id, EntityOnRemoteScimId externalId, ScimResourceType type) { + ScimResourceMapping entity = new ScimResourceMapping(); + entity.setType(type.name()); + entity.setExternalId(externalId.asString()); + entity.setComponentId(componentId); + entity.setRealmId(realmId); + entity.setId(id.asString()); + entityManager.persist(entity); + } + + private TypedQuery getScimResourceTypedQuery(String queryName, String id, ScimResourceType type) { + return getEntityManager() + .createNamedQuery(queryName, ScimResourceMapping.class) + .setParameter("type", type.name()) + .setParameter("realmId", getRealmId()) + .setParameter("componentId", getComponentId()) + .setParameter("id", id); + } + + public Optional findByExternalId(EntityOnRemoteScimId externalId, ScimResourceType type) { + try { + return Optional.of( + getScimResourceTypedQuery("findByExternalId", externalId.asString(), type).getSingleResult() + ); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findById(KeycloakId keycloakId, ScimResourceType type) { + try { + return Optional.of( + getScimResourceTypedQuery("findById", keycloakId.asString(), type).getSingleResult() + ); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findUserById(KeycloakId id) { + return findById(id, ScimResourceType.USER); + } + + public Optional findUserByExternalId(EntityOnRemoteScimId externalId) { + return findByExternalId(externalId, ScimResourceType.USER); + } + + public void delete(ScimResourceMapping resource) { + entityManager.remove(resource); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceId.java b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceId.java new file mode 100644 index 0000000000..d0abddf2b1 --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceId.java @@ -0,0 +1,83 @@ +package sh.libre.scim.jpa; + +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.util.Objects; + +public class ScimResourceId implements Serializable { + private String id; + private String realmId; + private String componentId; + private String type; + private String externalId; + + public ScimResourceId() { + } + + public ScimResourceId(String id, String realmId, String componentId, String type, String externalId) { + this.setId(id); + this.setRealmId(realmId); + this.setComponentId(componentId); + this.setType(type); + this.setExternalId(externalId); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getComponentId() { + return componentId; + } + + public void setComponentId(String componentId) { + this.componentId = componentId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (!(other instanceof ScimResourceId o)) + return false; + return (StringUtils.equals(o.id, id) && + StringUtils.equals(o.realmId, realmId) && + StringUtils.equals(o.componentId, componentId) && + StringUtils.equals(o.type, type) && + StringUtils.equals(o.externalId, externalId)); + } + + @Override + public int hashCode() { + return Objects.hash(realmId, componentId, type, id, externalId); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java new file mode 100644 index 0000000000..ade6848ccd --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceMapping.java @@ -0,0 +1,89 @@ +package sh.libre.scim.jpa; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; +import sh.libre.scim.core.service.EntityOnRemoteScimId; +import sh.libre.scim.core.service.KeycloakId; + +@Entity +@IdClass(ScimResourceId.class) +@Table(name = "SCIM_RESOURCE_MAPPING") +@NamedQueries({ + @NamedQuery(name = "findById", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and id = :id"), + @NamedQuery(name = "findByExternalId", query = "from ScimResourceMapping where realmId = :realmId and componentId = :componentId and type = :type and externalId = :id") +}) +public class ScimResourceMapping { + + @Id + @Column(name = "ID", nullable = false) + private String id; + + @Id + @Column(name = "REALM_ID", nullable = false) + private String realmId; + + @Id + @Column(name = "COMPONENT_ID", nullable = false) + private String componentId; + + @Id + @Column(name = "TYPE", nullable = false) + private String type; + + @Id + @Column(name = "EXTERNAL_ID", nullable = false) + private String externalId; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getComponentId() { + return componentId; + } + + public void setComponentId(String componentId) { + this.componentId = componentId; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public KeycloakId getIdAsKeycloakId() { + return new KeycloakId(id); + } + + public EntityOnRemoteScimId getExternalIdAsEntityOnRemoteScimId() { + return new EntityOnRemoteScimId(externalId); + } +} diff --git a/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java new file mode 100644 index 0000000000..6ef55a060e --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java @@ -0,0 +1,29 @@ +package sh.libre.scim.jpa; + +import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; + +import java.util.Collections; +import java.util.List; + +public class ScimResourceProvider implements JpaEntityProvider { + + @Override + public List> getEntities() { + return Collections.singletonList(ScimResourceMapping.class); + } + + @Override + public String getChangelogLocation() { + return "META-INF/scim-resource-changelog.xml"; + } + + @Override + public void close() { + // Nothing to close + } + + @Override + public String getFactoryId() { + return ScimResourceProviderFactory.ID; + } +} \ No newline at end of file diff --git a/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java new file mode 100644 index 0000000000..7f3cc323dd --- /dev/null +++ b/federation/scim/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java @@ -0,0 +1,39 @@ +package sh.libre.scim.jpa; + +import org.keycloak.Config.Scope; +import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; +import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +public class ScimResourceProviderFactory implements JpaEntityProviderFactory { + + static final String ID = "scim-resource"; + + @Override + public JpaEntityProvider create(KeycloakSession session) { + return new ScimResourceProvider(); + } + + @Override + public String getId() { + return ID; + } + + @Override + public void init(Scope scope) { + // Nothing to initialise + } + + @Override + public void postInit(KeycloakSessionFactory sessionFactory) { + // Nothing to do + } + + + @Override + public void close() { + // Nothing to close + } + +} diff --git a/federation/scim/src/main/resources/META-INF/jboss-deployment-structure.xml b/federation/scim/src/main/resources/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000000..42007fe638 --- /dev/null +++ b/federation/scim/src/main/resources/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/federation/scim/src/main/resources/META-INF/scim-resource-changelog.xml b/federation/scim/src/main/resources/META-INF/scim-resource-changelog.xml new file mode 100644 index 0000000000..d3e2687a57 --- /dev/null +++ b/federation/scim/src/main/resources/META-INF/scim-resource-changelog.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/federation/scim/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/federation/scim/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory new file mode 100644 index 0000000000..b3cb1a13e3 --- /dev/null +++ b/federation/scim/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory @@ -0,0 +1 @@ +sh.libre.scim.jpa.ScimResourceProviderFactory diff --git a/federation/scim/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/federation/scim/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory new file mode 100644 index 0000000000..7e2a6edd9c --- /dev/null +++ b/federation/scim/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory @@ -0,0 +1 @@ +sh.libre.scim.event.ScimEventListenerProviderFactory diff --git a/federation/scim/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/federation/scim/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 0000000000..308796c862 --- /dev/null +++ b/federation/scim/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -0,0 +1 @@ +sh.libre.scim.core.ScimEndpointConfigurationStorageProviderFactory diff --git a/pom.xml b/pom.xml index 1311f05029..0d4fca760d 100644 --- a/pom.xml +++ b/pom.xml @@ -926,6 +926,11 @@ keycloak-ldap-federation ${project.version} + + org.keycloak + keycloak-scim-federation + ${project.version} + org.keycloak keycloak-dependencies-server-min diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index 0155a9030d..86ad041451 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -324,6 +324,16 @@ + + org.keycloak + keycloak-scim-federation + + + * + * + + + org.keycloak keycloak-config-api