diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c41cc9e35e38efc7d080637859e6c72940b374a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..e0f15db2eb22b5d618150277e48b741f8fdd277a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..7bb40ee5d814eda2e82b4103d693e9ba5d0f995f --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9ec62e56e41a45c49e35149358ca2c9a404dbde8 --- /dev/null +++ b/pom.xml @@ -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> \ No newline at end of file diff --git a/src/main/java/sh/libre/scim/core/ScimClient.java b/src/main/java/sh/libre/scim/core/ScimClient.java new file mode 100644 index 0000000000000000000000000000000000000000..1e1533b593f823a1c3e5752d4fe0f85e2c8508d3 --- /dev/null +++ b/src/main/java/sh/libre/scim/core/ScimClient.java @@ -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(); + } +} diff --git a/src/main/java/sh/libre/scim/core/ScimDispatcher.java b/src/main/java/sh/libre/scim/core/ScimDispatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..40bef841e566a66a19d9bdaf9ec1a584b7c83baa --- /dev/null +++ b/src/main/java/sh/libre/scim/core/ScimDispatcher.java @@ -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); + } + } +} diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..ff23030ed2b65f3b97b3b8494b7a8f6714a5ee1f --- /dev/null +++ b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java @@ -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); + } +} diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..debe59b8fb75f30b9aecacbb5d83c5e2e83385cf --- /dev/null +++ b/src/main/java/sh/libre/scim/event/ScimEventListenerProviderFactory.java @@ -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"; + } +} diff --git a/src/main/java/sh/libre/scim/jpa/ScimResource.java b/src/main/java/sh/libre/scim/jpa/ScimResource.java new file mode 100644 index 0000000000000000000000000000000000000000..0034e257eba85d8bb22b02db47e6191aec12c4b7 --- /dev/null +++ b/src/main/java/sh/libre/scim/jpa/ScimResource.java @@ -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; + } +} diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceId.java b/src/main/java/sh/libre/scim/jpa/ScimResourceId.java new file mode 100644 index 0000000000000000000000000000000000000000..c97ff53a527b840a4522551c4300420a955063ea --- /dev/null +++ b/src/main/java/sh/libre/scim/jpa/ScimResourceId.java @@ -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); + } +} diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java b/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..3fcd97bd3cff647751725250fcba8d70e6181862 --- /dev/null +++ b/src/main/java/sh/libre/scim/jpa/ScimResourceProvider.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java b/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..682ccbee04842201c29852895927d24e815329a5 --- /dev/null +++ b/src/main/java/sh/libre/scim/jpa/ScimResourceProviderFactory.java @@ -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) { + } +} diff --git a/src/main/resources/META-INF/scim-resource-changelog.xml b/src/main/resources/META-INF/scim-resource-changelog.xml new file mode 100644 index 0000000000000000000000000000000000000000..954a44acb8eb0f9924b1920635f3c7543184137a --- /dev/null +++ b/src/main/resources/META-INF/scim-resource-changelog.xml @@ -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> \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory new file mode 100644 index 0000000000000000000000000000000000000000..b3cb1a13e34435fc9114e7cd02ab77d87b3c6871 --- /dev/null +++ b/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory @@ -0,0 +1 @@ +sh.libre.scim.jpa.ScimResourceProviderFactory diff --git a/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory new file mode 100644 index 0000000000000000000000000000000000000000..7e2a6edd9cccb0c544c7d6b9fbd65ffc3ff7324a --- /dev/null +++ b/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory @@ -0,0 +1 @@ +sh.libre.scim.event.ScimEventListenerProviderFactory