diff --git a/README.md b/README.md index 4e6889b03d9f4770ba7209948e6e28544fd74655..785758b00710b8755947fe56ce4d369bd736ffdf 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,59 @@ # keycloak-scim-client -This extension add [SCIM2](http://www.simplecloud.info) client capabilities to Keycloak. (See [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)). +This extension add [SCIM2](http://www.simplecloud.info) client capabilities to Keycloak. -## Overview +It allows to : -### Motivation +* Declare SCIM endpoints (through the identity federation UI). Any tool implementing SCIM protocol can be wired to the + Keycloack instance through this declaration. +* Propagate users and groups from Keycloack to SCIM endpoints : when a user/group gets created or modified in Keycloack, + the modification is fowarded 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 Keycloack synchronization mechanism). -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. +See [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) +and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)) for further details -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. +## Overview -### Technical choices +### Motivation -The SCIM protocol is standard, comprehensible and easy to implement. It's a perfect fit for our goal. +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. -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. +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 KC : -- Event Listener : it's used to listens for changes and transform them in SCIM calls. -- Federation Provider : it's used to set up all the SCIM service providers without creating our own UI. -- JPA Entity Provider : it's used to save the mapping between the local IDs and the service providers IDs. +This extension uses 3 concepts in KeyCloack : -Because the event listener is the source of the SCIM flow, and it is not cancelable, we can't have strictly consistent behavior in case of SCIM service provider failure. +- Event Listener : used to listen for changes within Keycloack (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. ## Usage -### Installation (quick) +### Development mode + +From the repository root : + +* Launch the docker-compose image (composed of a postgre and keycloack instance runing on localhost:8080) : + ``docker compose up -d`` +* Execute ``gradle jar shadowJar && docker compose restart keycloak`` to build extension and update the Keycloack + instance +* You can access extension logs through ``docker compose logs -f`` -1. Download the [latest version](https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package) +### Installation + +1. Download + the [latest version](https://lab.libreho.st/libre.sh/scim/keycloak-scim/-/jobs/artifacts/main/raw/build/libs/keycloak-scim-1.0-SNAPSHOT-all.jar?job=package) 2. Put it in `/opt/keycloak/providers/`. It's also possible to build your own custom image if you run Keycloak in a [container](/docs/container.md). @@ -38,7 +62,7 @@ Other [installation options](/docs/installation.md) are available. ### Setup -#### Add the event listerner +#### Enable SCIM Event listeners 1. Go to `Admin Console > Events > Config`. 2. Add `scim` in `Event Listeners`. @@ -46,37 +70,35 @@ Other [installation options](/docs/installation.md) are available.  -#### Create a federation provider +#### Register SCIM Service Providers -1. Go to `Admin Console > User Federation`. -2. Click on `Add provider`. -3. Select `scim`. -4. Configure the provider ([see](#configuration)). -5. Save. +1. Go to `Admin Console > Realm Settings > Events`. +2. Add `scim` to the list of event listers +3. Save  ### Configuration -Add the endpoint - for a local set up you have to add the two containers in a docker network and use the container ip see [here](https://docs.docker.com/engine/reference/commandline/network/) -If you use the [rocketchat app](https://lab.libreho.st/libre.sh/scim/rocketchat-scim) you get the endpoint from your rocket Chat Scim Adapter App Details. +Add the endpoint - for a local set up you have to add the two containers in a docker network and use the container ip +see [here](https://docs.docker.com/engine/reference/commandline/network/) +If you use the [rocketchat app](https://lab.libreho.st/libre.sh/scim/rocketchat-scim) you get the endpoint from your +rocket Chat Scim Adapter App Details. Endpoint content type is application/json. Auth mode Bearer or None for local test setup. Copy the bearer token from your app details in rocketchat. 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 necesarry. You can either do: +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 - **[License AGPL](/LICENSE)** diff --git a/build.gradle b/build.gradle index 34a263c2012b3e8566b8646b3663fecc0cb397ec..7897ff6a60548a0fa5baed04f395cd45a54af10a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ plugins { id 'java' id 'com.github.johnrengelman.shadow' version '8.1.1' - id "org.sonarqube" version "5.0.0.4638" + id "org.sonarqube" version "5.1.0.4882" + id "com.github.ben-manes.versions" version "0.51.0" } group = 'sh.libre.scim' @@ -16,12 +17,12 @@ repositories { } dependencies { - compileOnly 'org.keycloak:keycloak-core:23.0.4' - compileOnly 'org.keycloak:keycloak-server-spi:23.0.4' - compileOnly 'org.keycloak:keycloak-server-spi-private:23.0.4' - compileOnly 'org.keycloak:keycloak-services:23.0.4' - compileOnly 'org.keycloak:keycloak-model-jpa:23.0.4' + compileOnly 'org.keycloak:keycloak-core:26.0.1' + compileOnly 'org.keycloak:keycloak-server-spi:26.0.1' + compileOnly 'org.keycloak:keycloak-server-spi-private:26.0.1' + compileOnly 'org.keycloak:keycloak-services:26.0.1' + compileOnly 'org.keycloak:keycloak-model-jpa:26.0.1' implementation 'io.github.resilience4j:resilience4j-retry:2.2.0' - implementation 'de.captaingoldfish:scim-sdk-common:1.21.1' - implementation 'de.captaingoldfish:scim-sdk-client:1.21.1' + implementation 'de.captaingoldfish:scim-sdk-common:1.26.0' + implementation 'de.captaingoldfish:scim-sdk-client:1.26.0' } diff --git a/docker-compose.yml b/docker-compose.yml index d119247e91198a8c115a2660910efb3fcede8146..43e4f3a77023c85f6d4a0859eaf36b032e8b2b63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: ports: - 5432:5432 keycloak: - image: quay.io/keycloak/keycloak:23.0.3 + image: quay.io/keycloak/keycloak:26.0.1 build: . command: start-dev volumes: diff --git a/docs/img/event-listener-page.png b/docs/img/event-listener-page.png index 066b25d774d22caea0f7cfdef61a302c82ed13be..5c45734c68fcc9ecfe558cd84fb571c8fcffb20f 100644 Binary files a/docs/img/event-listener-page.png and b/docs/img/event-listener-page.png differ diff --git a/docs/img/federation-provider-page.png b/docs/img/federation-provider-page.png index e89858d432a3ed0f5c26a1a08672f1803fd138f1..390128f581b6bca7ea04d272ab71299788a99f79 100644 Binary files a/docs/img/federation-provider-page.png and b/docs/img/federation-provider-page.png differ diff --git a/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java b/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java index 993f287ff5a2157ec936bce5a2624ff99144dd9e..78973d2ad6e4ad9ee2fb32d73544d8786f2b6f39 100644 --- a/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java +++ b/src/main/java/sh/libre/scim/core/exceptions/ScimExceptionHandler.java @@ -37,7 +37,7 @@ public class ScimExceptionHandler { session.getTransactionManager().rollback(); LOGGER.error("TRANSACTION ROLLBACK - " + errorMessage, e); } else { - LOGGER.warn(errorMessage); + LOGGER.warn(errorMessage, e); } } } diff --git a/src/main/java/sh/libre/scim/core/service/AbstractScimService.java b/src/main/java/sh/libre/scim/core/service/AbstractScimService.java index b2109b48ec505751a23dcdb83a1757b2063d4e2c..b22b6a9a8d60ee58634940418ce437b4446f3c35 100644 --- a/src/main/java/sh/libre/scim/core/service/AbstractScimService.java +++ b/src/main/java/sh/libre/scim/core/service/AbstractScimService.java @@ -119,13 +119,13 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R syncRes.increaseUpdated(); } catch (InvalidResponseFromScimEndpointException e) { if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) { - LOGGER.warn("Error while syncing " + id + " to endpoint " + getConfiguration().getEndPoint()); + 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()); + LOGGER.warn("Inconsistent data for element " + id + " and endpoint " + getConfiguration().getEndPoint(), e); } else { throw e; } @@ -155,20 +155,20 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R } } catch (UnexpectedScimDataException e) { if (skipOrStopStrategy.skipInvalidDataFromScimEndpoint(getConfiguration())) { - LOGGER.warn("[SCIM] Skipping element synchronisation because of invalid Scim Data for element " + resource.getId() + " : " + e.getMessage()); + 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()); + 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"); + LOGGER.warn("[SCIM] Could not delete SCIM resource " + resource.getId() + " during synchronisation", e); } else { throw e; }