poc
This commit is contained in:
parent
e0e7060a30
commit
f2dbb59e0a
15 changed files with 801 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic"
|
||||||
|
}
|
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
volumes:
|
||||||
|
- db:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: keycloak
|
||||||
|
POSTGRES_PASSWORD: keycloak
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:16.1.1
|
||||||
|
volumes:
|
||||||
|
- ./target/keycloak-scim-1.0-SNAPSHOT-jar-with-dependencies.jar:/opt/jboss/keycloak/standalone/deployments/keycloak-scim-1.0-SNAPSHOT.jar
|
||||||
|
environment:
|
||||||
|
DB_VENDOR: POSTGRES
|
||||||
|
DB_ADDR: postgres
|
||||||
|
DB_DATABASE: keycloak
|
||||||
|
DB_USER: keycloak
|
||||||
|
DB_SCHEMA: public
|
||||||
|
DB_PASSWORD: keycloak
|
||||||
|
KEYCLOAK_USER: admin
|
||||||
|
KEYCLOAK_PASSWORD: admin
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db:
|
198
pom.xml
Normal file
198
pom.xml
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>sh.libre.scim</groupId>
|
||||||
|
<artifactId>keycloak-scim</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<name>keycloak-scim</name>
|
||||||
|
<!-- FIXME change it to the project's website -->
|
||||||
|
<url>http://www.example.com</url>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<keycloak.version>16.1.0</keycloak.version>
|
||||||
|
<kotlin.version>1.6.10</kotlin.version>
|
||||||
|
<jackson.version>2.12.1</jackson.version>
|
||||||
|
<resteasy.version>3.15.1.Final</resteasy.version>
|
||||||
|
<resilience4jVersion>1.7.0</resilience4jVersion>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<version>${keycloak.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-server-spi</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<version>${keycloak.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-server-spi-private</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<version>${keycloak.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-services</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<version>${keycloak.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-model-jpa</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
<version>${keycloak.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.unboundid.product.scim2</groupId>
|
||||||
|
<artifactId>scim2-sdk-client</artifactId>
|
||||||
|
<version>2.3.7</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.ws.rs</groupId>
|
||||||
|
<artifactId>javax.ws.rs-api</artifactId>
|
||||||
|
<version>2.1.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.xml.bind</groupId>
|
||||||
|
<artifactId>jaxb-api</artifactId>
|
||||||
|
<version>2.3.1</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.module</groupId>
|
||||||
|
<artifactId>jackson-module-jaxb-annotations</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.jaxrs</groupId>
|
||||||
|
<artifactId>jackson-jaxrs-json-provider</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.jaxrs</groupId>
|
||||||
|
<artifactId>jackson-jaxrs-base</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-jaxrs</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-multipart-provider</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-jackson2-provider</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-jaxb-provider</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-client</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-retry</artifactId>
|
||||||
|
<version>${resilience4jVersion}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate</groupId>
|
||||||
|
<artifactId>hibernate-core</artifactId>
|
||||||
|
<version>5.6.5.Final</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate</groupId>
|
||||||
|
<artifactId>hibernate-validator</artifactId>
|
||||||
|
<version>7.0.2.Final</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.wildfly.plugins</groupId>
|
||||||
|
<artifactId>wildfly-maven-plugin</artifactId>
|
||||||
|
<version>2.1.0</version>
|
||||||
|
<configuration>
|
||||||
|
<skip>false</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>2.6</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>make-assembly</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>${main.class}</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
156
src/main/java/sh/libre/scim/core/ScimClient.java
Normal file
156
src/main/java/sh/libre/scim/core/ScimClient.java
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import com.unboundid.scim2.client.ScimService;
|
||||||
|
import com.unboundid.scim2.common.exceptions.ScimException;
|
||||||
|
import com.unboundid.scim2.common.types.Email;
|
||||||
|
import com.unboundid.scim2.common.types.Meta;
|
||||||
|
import com.unboundid.scim2.common.types.Name;
|
||||||
|
import com.unboundid.scim2.common.types.UserResource;
|
||||||
|
import io.github.resilience4j.core.IntervalFunction;
|
||||||
|
import io.github.resilience4j.retry.RetryConfig;
|
||||||
|
import io.github.resilience4j.retry.RetryRegistry;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.lang.RuntimeException;
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.persistence.NoResultException;
|
||||||
|
import javax.persistence.TypedQuery;
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import sh.libre.scim.jpa.ScimResource;
|
||||||
|
|
||||||
|
public class ScimClient {
|
||||||
|
|
||||||
|
final Logger LOGGER = Logger.getLogger(ScimClient.class);
|
||||||
|
final Client client = ResteasyClientBuilder.newClient();
|
||||||
|
final ScimService scimService;
|
||||||
|
final RetryRegistry registry;
|
||||||
|
final String name;
|
||||||
|
final String realmId;
|
||||||
|
final EntityManager entityManager;
|
||||||
|
|
||||||
|
public ScimClient(String name, String url, String realmId, EntityManager entityManager) {
|
||||||
|
this.name = name;
|
||||||
|
this.realmId = realmId;
|
||||||
|
this.entityManager = entityManager;
|
||||||
|
var target = client.target(url);
|
||||||
|
scimService = new ScimService(target);
|
||||||
|
|
||||||
|
RetryConfig retryConfig = RetryConfig.custom()
|
||||||
|
.maxAttempts(10)
|
||||||
|
.intervalFunction(IntervalFunction.ofExponentialBackoff())
|
||||||
|
.build();
|
||||||
|
registry = RetryRegistry.of(retryConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createUser(UserModel kcUser) {
|
||||||
|
LOGGER.info("Create User");
|
||||||
|
var user = toUser(kcUser);
|
||||||
|
var retry = registry.retry("create-" + kcUser.getId());
|
||||||
|
var spUser = retry.executeSupplier(() -> {
|
||||||
|
try {
|
||||||
|
return scimService.create("Users", user);
|
||||||
|
} catch (ScimException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var scimUser = toScimUser(spUser);
|
||||||
|
entityManager.persist(scimUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceUser(UserModel kcUser) {
|
||||||
|
LOGGER.info("Replace User");
|
||||||
|
try {
|
||||||
|
var resource = querUserById(kcUser.getId());
|
||||||
|
var user = toUser(kcUser);
|
||||||
|
user.setId(resource.getRemoteId());
|
||||||
|
var meta = new Meta();
|
||||||
|
var uri = new URI("Users/" + user.getId());
|
||||||
|
meta.setLocation(uri);
|
||||||
|
user.setMeta(meta);
|
||||||
|
var retry = registry.retry("replace-" + kcUser.getId());
|
||||||
|
retry.executeSupplier(() -> {
|
||||||
|
try {
|
||||||
|
return scimService.replace(user);
|
||||||
|
} catch (ScimException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
LOGGER.warnf("Failde to replce user %s, scim mapping not found", kcUser.getId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteUser(String userId) {
|
||||||
|
LOGGER.info("Delete User");
|
||||||
|
try {
|
||||||
|
var resource = querUserById(userId);
|
||||||
|
var retry = registry.retry("delete-" + userId);
|
||||||
|
retry.executeSupplier(() -> {
|
||||||
|
try {
|
||||||
|
scimService.delete("Users", resource.getRemoteId());
|
||||||
|
} catch (ScimException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
entityManager.remove(resource);
|
||||||
|
} catch (NoResultException e) {
|
||||||
|
LOGGER.warnf("Failde to replce user %s, scim mapping not found", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypedQuery<ScimResource> queryUser(String query) {
|
||||||
|
return entityManager
|
||||||
|
.createNamedQuery(query, ScimResource.class)
|
||||||
|
.setParameter("realmId", realmId)
|
||||||
|
.setParameter("type", "Users")
|
||||||
|
.setParameter("serviceProvider", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScimResource querUserById(String id) {
|
||||||
|
return queryUser("findByLocalId").setParameter("id", id).getSingleResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScimResource scimUser() {
|
||||||
|
var resource = new ScimResource();
|
||||||
|
resource.setType("Users");
|
||||||
|
resource.setRealmId(realmId);
|
||||||
|
resource.setServiceProvider(name);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScimResource toScimUser(UserResource user) {
|
||||||
|
var resource = scimUser();
|
||||||
|
resource.setRemoteId(user.getId());
|
||||||
|
resource.setLocalId(user.getExternalId());
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserResource toUser(UserModel kcUser) {
|
||||||
|
var user = new UserResource();
|
||||||
|
user.setExternalId(kcUser.getId());
|
||||||
|
user.setUserName(kcUser.getUsername());
|
||||||
|
var name = new Name();
|
||||||
|
name.setGivenName(kcUser.getFirstName());
|
||||||
|
name.setFamilyName(kcUser.getLastName());
|
||||||
|
user.setName(name);
|
||||||
|
|
||||||
|
var emails = new ArrayList<Email>();
|
||||||
|
if (kcUser.getEmail() != "") {
|
||||||
|
var email = new Email().setPrimary(true).setValue(kcUser.getEmail());
|
||||||
|
emails.add(email);
|
||||||
|
}
|
||||||
|
user.setEmails(emails);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
58
src/main/java/sh/libre/scim/core/ScimDispatcher.java
Normal file
58
src/main/java/sh/libre/scim/core/ScimDispatcher.java
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package sh.libre.scim.core;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
|
||||||
|
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
|
||||||
|
public class ScimDispatcher {
|
||||||
|
final KeycloakSession session;
|
||||||
|
final EntityManager entityManager;
|
||||||
|
final Logger LOGGER = Logger.getLogger(ScimDispatcher.class);
|
||||||
|
ArrayList<ScimClient> clients = new ArrayList<ScimClient>();
|
||||||
|
|
||||||
|
public ScimDispatcher(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
entityManager = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||||
|
reloadClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadClients() {
|
||||||
|
close();
|
||||||
|
LOGGER.info("Cleared SCIM Clients");
|
||||||
|
var realm = session.getContext().getRealm();
|
||||||
|
clients = new ArrayList<ScimClient>();
|
||||||
|
var kcClients = session.clients().getClientsStream(realm);
|
||||||
|
for (var kcClient : kcClients.toList()) {
|
||||||
|
var endpoint = kcClient.getAttribute("scim-endpoint");
|
||||||
|
var name = kcClient.getAttribute("scim-name");
|
||||||
|
if (endpoint != "") {
|
||||||
|
if (name == "") {
|
||||||
|
name = kcClient.getName();
|
||||||
|
}
|
||||||
|
clients.add(new ScimClient(
|
||||||
|
name,
|
||||||
|
endpoint,
|
||||||
|
realm.getId(),
|
||||||
|
entityManager));
|
||||||
|
LOGGER.infof("Added %s SCIM Client (%s)", name, endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
for (var client : clients) {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run(Consumer<ScimClient> f) {
|
||||||
|
for (var client : clients) {
|
||||||
|
f.accept(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package sh.libre.scim.event;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
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.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
import sh.libre.scim.core.ScimDispatcher;
|
||||||
|
|
||||||
|
public class ScimEventListenerProvider implements EventListenerProvider {
|
||||||
|
final Logger LOGGER = Logger.getLogger(ScimEventListenerProvider.class);
|
||||||
|
ScimDispatcher dispatcher;
|
||||||
|
KeycloakSession session;
|
||||||
|
|
||||||
|
public ScimEventListenerProvider(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
dispatcher = new ScimDispatcher(session);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
dispatcher.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(Event event) {
|
||||||
|
if (event.getType() == EventType.REGISTER) {
|
||||||
|
var user = getUser(event.getUserId());
|
||||||
|
dispatcher.run((client) -> client.createUser(user));
|
||||||
|
}
|
||||||
|
if (event.getType() == EventType.UPDATE_EMAIL || event.getType() == EventType.UPDATE_PROFILE) {
|
||||||
|
var user = getUser(event.getUserId());
|
||||||
|
dispatcher.run((client) -> client.replaceUser(user));
|
||||||
|
}
|
||||||
|
if (event.getType() == EventType.DELETE_ACCOUNT) {
|
||||||
|
dispatcher.run((client) -> client.deleteUser(event.getUserId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(AdminEvent event, boolean includeRepresentation) {
|
||||||
|
if (event.getResourceType() == ResourceType.CLIENT) {
|
||||||
|
dispatcher.reloadClients();
|
||||||
|
}
|
||||||
|
if (event.getResourceType() == ResourceType.USER) {
|
||||||
|
var userId = event.getResourcePath().replace("users/", "");
|
||||||
|
LOGGER.infof("%s %s", userId, event.getOperationType());
|
||||||
|
if (event.getOperationType() == OperationType.CREATE) {
|
||||||
|
// session.getTransactionManager().rollback();
|
||||||
|
var user = getUser(userId);
|
||||||
|
dispatcher.run((client) -> client.createUser(user));
|
||||||
|
}
|
||||||
|
if (event.getOperationType() == OperationType.UPDATE) {
|
||||||
|
var user = getUser(userId);
|
||||||
|
dispatcher.run((client) -> client.replaceUser(user));
|
||||||
|
}
|
||||||
|
if (event.getOperationType() == OperationType.DELETE) {
|
||||||
|
dispatcher.run((client) -> client.deleteUser(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserModel getUser(String id) {
|
||||||
|
return session.users().getUserById(session.getContext().getRealm(), id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
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 void init(Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "scim";
|
||||||
|
}
|
||||||
|
}
|
88
src/main/java/sh/libre/scim/jpa/ScimResource.java
Normal file
88
src/main/java/sh/libre/scim/jpa/ScimResource.java
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package sh.libre.scim.jpa;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.IdClass;
|
||||||
|
import javax.persistence.NamedQuery;
|
||||||
|
import javax.persistence.NamedQueries;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@IdClass(ScimResourceId.class)
|
||||||
|
@Table(name = "SCIM_RESOURCE")
|
||||||
|
@NamedQueries({
|
||||||
|
@NamedQuery(name = "findByLocalId", query = "from ScimResource where realmId = :realmId and type = :type and serviceProvider = :serviceProvider and localId = :id"),
|
||||||
|
@NamedQuery(name = "findByRemoteId", query = "from ScimResource where realmId = :realmId and type = :type and serviceProvider = :serviceProvider and remoteId = :id") })
|
||||||
|
public class ScimResource {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "REALM_ID", nullable = false)
|
||||||
|
private String realmId;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "SERVICE_PROVIDER", nullable = false)
|
||||||
|
private String serviceProvider;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "TYPE", nullable = false)
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "REMOTE_ID", nullable = false)
|
||||||
|
private String remoteId;
|
||||||
|
|
||||||
|
@Column(name = "LOCAL_ID", nullable = false)
|
||||||
|
private String localId;
|
||||||
|
|
||||||
|
public ScimResource() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimResource(String realmId, String serviceProvider, String type, String remoteId, String localId) {
|
||||||
|
this.realmId = realmId;
|
||||||
|
this.serviceProvider = serviceProvider;
|
||||||
|
this.type = type;
|
||||||
|
this.remoteId = remoteId;
|
||||||
|
this.localId = localId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRealmId() {
|
||||||
|
return realmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRealmId(String realmId) {
|
||||||
|
this.realmId = realmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServiceProvider() {
|
||||||
|
return serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServiceProvider(String serviceProvider) {
|
||||||
|
this.serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemoteId() {
|
||||||
|
return remoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemoteId(String remoteId) {
|
||||||
|
this.remoteId = remoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocalId() {
|
||||||
|
return localId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocalId(String localId) {
|
||||||
|
this.localId = localId;
|
||||||
|
}
|
||||||
|
}
|
71
src/main/java/sh/libre/scim/jpa/ScimResourceId.java
Normal file
71
src/main/java/sh/libre/scim/jpa/ScimResourceId.java
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package sh.libre.scim.jpa;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ScimResourceId implements Serializable {
|
||||||
|
private String realmId;
|
||||||
|
private String serviceProvider;
|
||||||
|
private String type;
|
||||||
|
private String remoteId;
|
||||||
|
|
||||||
|
public ScimResourceId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScimResourceId(String realmId, String serviceProvider, String type, String remoteId) {
|
||||||
|
this.realmId = realmId;
|
||||||
|
this.serviceProvider = serviceProvider;
|
||||||
|
this.type = type;
|
||||||
|
this.remoteId = remoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRealmId() {
|
||||||
|
return realmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRealmId(String realmId) {
|
||||||
|
this.realmId = realmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServiceProvider() {
|
||||||
|
return serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServiceProvider(String serviceProvider) {
|
||||||
|
this.serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemoteId() {
|
||||||
|
return realmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemoteId(String remoteId) {
|
||||||
|
this.remoteId = remoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (this == other)
|
||||||
|
return true;
|
||||||
|
if (!(other instanceof ScimResourceId))
|
||||||
|
return false;
|
||||||
|
var o = (ScimResourceId) other;
|
||||||
|
return (o.realmId == realmId &&
|
||||||
|
o.serviceProvider == serviceProvider &&
|
||||||
|
o.type == type &&
|
||||||
|
o.remoteId == remoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(realmId, serviceProvider, type, remoteId);
|
||||||
|
}
|
||||||
|
}
|
29
src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java
Normal file
29
src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package sh.libre.scim.jpa;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class ScimResourceProvider implements JpaEntityProvider {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Class<?>> getEntities() {
|
||||||
|
return Arrays.asList(ScimResource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getChangelogLocation() {
|
||||||
|
return "META-INF/scim-resource-changelog.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFactoryId() {
|
||||||
|
return ScimResourceProviderFactory.ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
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 {
|
||||||
|
final static String ID ="scim-resource";
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JpaEntityProvider create(KeycloakSession session) {
|
||||||
|
return new ScimResourceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Scope scope) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory sessionFactory) {
|
||||||
|
}
|
||||||
|
}
|
27
src/main/resources/META-INF/scim-resource-changelog.xml
Normal file
27
src/main/resources/META-INF/scim-resource-changelog.xml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||||
|
<changeSet author="contact@indiehosters.net" id="scim-resource-1.0">
|
||||||
|
|
||||||
|
<createTable tableName="SCIM_RESOURCE">
|
||||||
|
<column name="REALM_ID" type="VARCHAR(36)">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
<column name="SERVICE_PROVIDER" type="VARCHAR(36)">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
<column name="TYPE" type="VARCHAR(36)">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
<column name="REMOTE_ID" type="VARCHAR(36)">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
<column name="LOCAL_ID" type="VARCHAR(36)">
|
||||||
|
<constraints nullable="false" />
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="PK_SCIM_RESOURCE" tableName="SCIM_RESOURCE" columnNames="REALM_ID,SERVICE_PROVIDER,TYPE,REMOTE_ID" />
|
||||||
|
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
|
@ -0,0 +1 @@
|
||||||
|
sh.libre.scim.jpa.ScimResourceProviderFactory
|
|
@ -0,0 +1 @@
|
||||||
|
sh.libre.scim.event.ScimEventListenerProviderFactory
|
Loading…
Reference in a new issue