Update to Keycloack 26 and scim-dsk 1.26

This commit is contained in:
Alex Morel 2024-10-22 09:26:28 +02:00
parent 7d87d23345
commit e8177237e0
7 changed files with 67 additions and 44 deletions

View file

@ -1,35 +1,59 @@
# keycloak-scim-client # 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.
It allows to :
* 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).
See [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643)
and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)) for further details
## Overview ## Overview
### Motivation ### 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. 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. 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.
### Technical choices 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.
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 #### Keycloak specific
This extension uses 3 concepts in KC : This extension uses 3 concepts in KeyCloack :
- 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.
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 ## Usage
### Installation (quick) ### Development mode
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) 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``
### 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/`. 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). 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 ### Setup
#### Add the event listerner #### Enable SCIM Event listeners
1. Go to `Admin Console > Events > Config`. 1. Go to `Admin Console > Events > Config`.
2. Add `scim` in `Event Listeners`. 2. Add `scim` in `Event Listeners`.
@ -46,37 +70,35 @@ Other [installation options](/docs/installation.md) are available.
![Event listener page](/docs/img/event-listener-page.png) ![Event listener page](/docs/img/event-listener-page.png)
#### Create a federation provider #### Register SCIM Service Providers
1. Go to `Admin Console > User Federation`. 1. Go to `Admin Console > Realm Settings > Events`.
2. Click on `Add provider`. 2. Add `scim` to the list of event listers
3. Select `scim`. 3. Save
4. Configure the provider ([see](#configuration)).
5. Save.
![Federation provider page](/docs/img/federation-provider-page.png) ![Federation provider page](/docs/img/federation-provider-page.png)
### Configuration ### 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/) Add the endpoint - for a local set up you have to add the two containers in a docker network and use the container ip
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. 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. Endpoint content type is application/json.
Auth mode Bearer or None for local test setup. Auth mode Bearer or None for local test setup.
Copy the bearer token from your app details in rocketchat. 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: If you enable import during sync then you can choose between to following import actions:
- Create Local - adds users to keycloak - Create Local - adds users to keycloak
- Nothing - Nothing
- Delete Remote - deletes users from the remote application - Delete Remote - deletes users from the remote application
### Sync ### 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 Full Sync
- Periodic Changed User Sync - Periodic Changed User Sync
**[License AGPL](/LICENSE)** **[License AGPL](/LICENSE)**

View file

@ -1,7 +1,8 @@
plugins { plugins {
id 'java' id 'java'
id 'com.github.johnrengelman.shadow' version '8.1.1' 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' group = 'sh.libre.scim'
@ -16,12 +17,12 @@ repositories {
} }
dependencies { dependencies {
compileOnly 'org.keycloak:keycloak-core:23.0.4' compileOnly 'org.keycloak:keycloak-core:26.0.1'
compileOnly 'org.keycloak:keycloak-server-spi:23.0.4' compileOnly 'org.keycloak:keycloak-server-spi:26.0.1'
compileOnly 'org.keycloak:keycloak-server-spi-private:23.0.4' compileOnly 'org.keycloak:keycloak-server-spi-private:26.0.1'
compileOnly 'org.keycloak:keycloak-services:23.0.4' compileOnly 'org.keycloak:keycloak-services:26.0.1'
compileOnly 'org.keycloak:keycloak-model-jpa:23.0.4' compileOnly 'org.keycloak:keycloak-model-jpa:26.0.1'
implementation 'io.github.resilience4j:resilience4j-retry:2.2.0' implementation 'io.github.resilience4j:resilience4j-retry:2.2.0'
implementation 'de.captaingoldfish:scim-sdk-common:1.21.1' implementation 'de.captaingoldfish:scim-sdk-common:1.26.0'
implementation 'de.captaingoldfish:scim-sdk-client:1.21.1' implementation 'de.captaingoldfish:scim-sdk-client:1.26.0'
} }

View file

@ -11,7 +11,7 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
keycloak: keycloak:
image: quay.io/keycloak/keycloak:23.0.3 image: quay.io/keycloak/keycloak:26.0.1
build: . build: .
command: start-dev command: start-dev
volumes: volumes:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View file

@ -37,7 +37,7 @@ public class ScimExceptionHandler {
session.getTransactionManager().rollback(); session.getTransactionManager().rollback();
LOGGER.error("TRANSACTION ROLLBACK - " + errorMessage, e); LOGGER.error("TRANSACTION ROLLBACK - " + errorMessage, e);
} else { } else {
LOGGER.warn(errorMessage); LOGGER.warn(errorMessage, e);
} }
} }
} }

View file

@ -119,13 +119,13 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
syncRes.increaseUpdated(); syncRes.increaseUpdated();
} catch (InvalidResponseFromScimEndpointException e) { } catch (InvalidResponseFromScimEndpointException e) {
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) { 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 { } else {
throw e; throw e;
} }
} catch (InconsistentScimMappingException e) { } catch (InconsistentScimMappingException e) {
if (skipOrStopStrategy.allowPartialSynchronizationWhenPushingToScim(this.getConfiguration())) { 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 { } else {
throw e; throw e;
} }
@ -155,20 +155,20 @@ public abstract class AbstractScimService<K extends RoleMapperModel, S extends R
} }
} catch (UnexpectedScimDataException e) { } catch (UnexpectedScimDataException e) {
if (skipOrStopStrategy.skipInvalidDataFromScimEndpoint(getConfiguration())) { 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 { } else {
throw e; throw e;
} }
} catch (InconsistentScimMappingException e) { } catch (InconsistentScimMappingException e) {
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) { 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 { } else {
throw e; throw e;
} }
} catch (InvalidResponseFromScimEndpointException e) { } catch (InvalidResponseFromScimEndpointException e) {
// Can only occur in case of a DELETE_REMOTE conflict action // Can only occur in case of a DELETE_REMOTE conflict action
if (skipOrStopStrategy.allowPartialSynchronizationWhenPullingFromScim(getConfiguration())) { 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 { } else {
throw e; throw e;
} }