From 182824a3b4504cf1d810aba7669949e0e4c476ab Mon Sep 17 00:00:00 2001 From: Hugo Renard Date: Tue, 6 Feb 2024 12:13:30 +0100 Subject: [PATCH] feat: update to keycloak 23 + varrious fixes --- build.gradle | 28 +++----- docker-compose.yml | 3 +- legacy-build.gradle | 33 --------- src/main/java/sh/libre/scim/core/Adapter.java | 15 ++-- .../java/sh/libre/scim/core/GroupAdapter.java | 48 +++++++------ .../java/sh/libre/scim/core/ScimClient.java | 46 +++++-------- .../sh/libre/scim/core/ScimDispatcher.java | 5 +- .../java/sh/libre/scim/core/UserAdapter.java | 68 ++++++++++++------- .../scim/event/ScimEventListenerProvider.java | 9 +-- .../java/sh/libre/scim/jpa/ScimResource.java | 14 ++-- .../sh/libre/scim/jpa/ScimResourceId.java | 1 + .../storage/ScimStorageProviderFactory.java | 34 +++++++++- 12 files changed, 153 insertions(+), 151 deletions(-) delete mode 100644 legacy-build.gradle diff --git a/build.gradle b/build.gradle index 35ee7731c4..ce8c8382a6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ plugins { id 'java' - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } group = 'sh.libre.scim' version = '1.0-SNAPSHOT' description = 'keycloak-scim' -java.sourceCompatibility = JavaVersion.VERSION_11 +java.sourceCompatibility = JavaVersion.VERSION_17 repositories { mavenLocal() @@ -15,20 +15,12 @@ repositories { } dependencies { - compileOnly 'org.keycloak:keycloak-core:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0' - compileOnly 'org.keycloak:keycloak-services:18.0.0' - compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0' - implementation 'io.github.resilience4j:resilience4j-retry:1.7.1' - implementation 'de.captaingoldfish:scim-sdk-common:1.15.3' - - implementation files('/home/marcportabella/Documents/totmicro/Repos/keycloak-scim-aws/scim-sdk-client-1.15.4-SNAPSHOT.jar') - //implementation 'de.captaingoldfish:scim-sdk-client:1.15.3' - implementation('org.wildfly.client:wildfly-client-config:1.0.1.Final') { - transitive false - } - implementation('org.jboss.resteasy:resteasy-client:4.7.6.Final') { - transitive false - } + 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' + 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' } diff --git a/docker-compose.yml b/docker-compose.yml index d18aa1ef3e..d119247e91 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: ports: - 5432:5432 keycloak: - image: quay.io/keycloak/keycloak:18.0.0 + image: quay.io/keycloak/keycloak:23.0.3 build: . command: start-dev volumes: @@ -23,6 +23,7 @@ services: KC_DB_PASSWORD: keycloak KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin + KC_LOG_LEVEL: INFO,sh.libre.scim:debug,de.captaingoldfish.scim:debug ports: - 127.0.0.1:8080:8080 depends_on: diff --git a/legacy-build.gradle b/legacy-build.gradle deleted file mode 100644 index 2f57e15a76..0000000000 --- a/legacy-build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -plugins { - id 'java' - id 'com.github.johnrengelman.shadow' version '7.1.2' -} - -group = 'sh.libre.scim' -version = '1.0-SNAPSHOT' -description = 'keycloak-scim' - -java.sourceCompatibility = JavaVersion.VERSION_11 - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - compileOnly 'org.keycloak:keycloak-core:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi:18.0.0' - compileOnly 'org.keycloak:keycloak-server-spi-private:18.0.0' - compileOnly 'org.keycloak:keycloak-services:18.0.0' - compileOnly 'org.keycloak:keycloak-model-jpa:18.0.0' - implementation 'io.github.resilience4j:resilience4j-retry:1.7.1' - - compileOnly 'org.wildfly.client:wildfly-client-config:1.0.1.Final' - compileOnly 'org.jboss.resteasy:resteasy-client:4.7.6.Final' - compileOnly 'org.jboss.resteasy:resteasy-client-api:4.7.6.Final' - -} - -shadowJar { - archiveClassifier.set('all-legacy') -} diff --git a/src/main/java/sh/libre/scim/core/Adapter.java b/src/main/java/sh/libre/scim/core/Adapter.java index 8faac5a3a7..d5cbb0d7a0 100644 --- a/src/main/java/sh/libre/scim/core/Adapter.java +++ b/src/main/java/sh/libre/scim/core/Adapter.java @@ -2,10 +2,9 @@ package sh.libre.scim.core; import java.util.stream.Stream; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.TypedQuery; -import javax.ws.rs.NotFoundException; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.TypedQuery; import org.jboss.logging.Logger; import org.keycloak.connections.jpa.JpaConnectionProvider; @@ -61,7 +60,7 @@ public abstract class Adapter } public String getSCIMEndpoint() { - return type + "s"; + return "/" + type + "s"; } public ScimResource toMapping() { @@ -95,12 +94,8 @@ public abstract class Adapter if (this.externalId != null) { return this.query("findByExternalId", externalId).getSingleResult(); } - } catch (NotFoundException e) { } catch (NoResultException e) { - } catch (Exception e) { - LOGGER.error(e); } - return null; } @@ -124,7 +119,7 @@ public abstract class Adapter public abstract Class getResourceClass(); - public abstract S toSCIM(Boolean addMeta); + public abstract S toSCIM(); public abstract Boolean entityExists(); diff --git a/src/main/java/sh/libre/scim/core/GroupAdapter.java b/src/main/java/sh/libre/scim/core/GroupAdapter.java index 86800606ca..e7edf6d7c8 100644 --- a/src/main/java/sh/libre/scim/core/GroupAdapter.java +++ b/src/main/java/sh/libre/scim/core/GroupAdapter.java @@ -2,16 +2,15 @@ package sh.libre.scim.core; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.persistence.NoResultException; +import jakarta.persistence.NoResultException; import de.captaingoldfish.scim.sdk.common.resources.Group; import de.captaingoldfish.scim.sdk.common.resources.multicomplex.Member; import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; @@ -59,44 +58,49 @@ public class GroupAdapter extends Adapter { if (groupMembers != null && groupMembers.size() > 0) { this.members = new HashSet(); for (var groupMember : groupMembers) { - var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User") - .getSingleResult(); - this.members.add(userMapping.getId()); + try { + var userMapping = this.query("findByExternalId", groupMember.getValue().get(), "User") + .getSingleResult(); + this.members.add(userMapping.getId()); + } catch (NoResultException e) { + LOGGER.warnf("member %s not found for scim group %s", groupMember.getValue().get(), group.getId().get()); + } } } } @Override - public Group toSCIM(Boolean addMeta) { + public Group toSCIM() { var group = new Group(); group.setId(externalId); group.setExternalId(id); group.setDisplayName(displayName); if (members.size() > 0) { - var groupMembers = new ArrayList(); for (var member : members) { var groupMember = new Member(); try { var userMapping = this.query("findById", member, "User").getSingleResult(); + LOGGER.debug(userMapping.getExternalId()); + LOGGER.debug(userMapping.getId()); groupMember.setValue(userMapping.getExternalId()); var ref = new URI(String.format("Users/%s", userMapping.getExternalId())); groupMember.setRef(ref.toString()); - groupMembers.add(groupMember); - } catch (Exception e) { - LOGGER.error(e); + group.addMember(groupMember); + } catch (NoResultException e) { + LOGGER.warnf("member %s not found for group %s", member, id); + } catch (URISyntaxException e) { + LOGGER.warnf("bad ref uri"); } } - group.setMembers(groupMembers); } - if (addMeta) { - var meta = new Meta(); - try { - var uri = new URI("Groups/" + externalId); - meta.setLocation(uri.toString()); - } catch (URISyntaxException e) { - } - group.setMeta(meta); + var meta = new Meta(); + try { + var uri = new URI("Groups/" + externalId); + meta.setLocation(uri.toString()); + } catch (URISyntaxException e) { + LOGGER.warn(e); } + group.setMeta(meta); return group; } @@ -114,7 +118,9 @@ public class GroupAdapter extends Adapter { @Override public Boolean tryToMap() { - var group = session.groups().getGroupsStream(realm).filter(x -> x.getName() == displayName).findFirst(); + var group = session.groups().getGroupsStream(realm).filter( + x -> StringUtils.equals(x.getName(), externalId) || StringUtils.equals(x.getName(), displayName)) + .findFirst(); if (group.isPresent()) { setId(group.get().getId()); return true; diff --git a/src/main/java/sh/libre/scim/core/ScimClient.java b/src/main/java/sh/libre/scim/core/ScimClient.java index 29c5c17f15..e3b1b72c35 100644 --- a/src/main/java/sh/libre/scim/core/ScimClient.java +++ b/src/main/java/sh/libre/scim/core/ScimClient.java @@ -3,9 +3,9 @@ package sh.libre.scim.core; import java.util.HashMap; import java.util.Map; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.ws.rs.ProcessingException; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.ws.rs.ProcessingException; import de.captaingoldfish.scim.sdk.client.ScimClientConfig; import de.captaingoldfish.scim.sdk.client.ScimRequestBuilder; @@ -51,12 +51,11 @@ public class ScimClient { switch (model.get("auth-mode")) { case "BEARER": defaultHeaders.put(HttpHeaders.AUTHORIZATION, - BearerAuthentication(model.get("auth-pass"))); + BearerAuthentication()); break; case "BASIC_AUTH": defaultHeaders.put(HttpHeaders.AUTHORIZATION, - BasicAuthentication(model.get("auth-user"), - model.get("auth-pass"))); + BasicAuthentication()); break; } @@ -73,10 +72,10 @@ public class ScimClient { registry = RetryRegistry.of(retryConfig); } - protected String BasicAuthentication(String username ,String password) { + protected String BasicAuthentication() { return BasicAuth.builder() - .username(model.get(username)) - .password(model.get(password)) + .username(model.get("auth-user")) + .password(model.get("auth-pass")) .build() .getAuthorizationHeaderValue(); } @@ -92,17 +91,10 @@ public class ScimClient { .build(); } - protected String BearerAuthentication(String token) { - return "Bearer " + token ; + protected String BearerAuthentication() { + return "Bearer " + model.get("auth-pass") ; } - protected String genScimUrl(String scimEndpoint,String resourcePath) { - return String.format("%s%s/%s", scimApplicationBaseUrl , - scimEndpoint, - resourcePath); - } - - protected EntityManager getEM() { return session.getProvider(JpaConnectionProvider.class).getEntityManager(); } @@ -136,8 +128,8 @@ public class ScimClient { ServerResponse response = retry.executeSupplier(() -> { try { return scimRequestBuilder - .create(adapter.getResourceClass(), String.format("/" + adapter.getSCIMEndpoint())) - .setResource(adapter.toSCIM(false)) + .create(adapter.getResourceClass(), adapter.getSCIMEndpoint()) + .setResource(adapter.toSCIM()) .sendRequest(); } catch ( ResponseException e) { throw new RuntimeException(e); @@ -166,9 +158,8 @@ public class ScimClient { ServerResponse response = retry.executeSupplier(() -> { try { return scimRequestBuilder - .update(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()), - adapter.getResourceClass()) - .setResource(adapter.toSCIM(false)) + .update(adapter.getResourceClass(), adapter.getSCIMEndpoint(), adapter.getExternalId()) + .setResource(adapter.toSCIM()) .sendRequest() ; } catch ( ResponseException e) { @@ -199,8 +190,7 @@ public class ScimClient { ServerResponse response = retry.executeSupplier(() -> { try { - return scimRequestBuilder.delete(genScimUrl(adapter.getSCIMEndpoint(), adapter.getExternalId()), - adapter.getResourceClass()) + return scimRequestBuilder.delete(adapter.getResourceClass(), adapter.getSCIMEndpoint(), adapter.getExternalId()) .sendRequest(); } catch (ResponseException e) { throw new RuntimeException(e); @@ -247,7 +237,7 @@ public class ScimClient { LOGGER.info("Import"); try { var adapter = getAdapter(aClass); - ServerResponse> response = scimRequestBuilder.list("url",adapter.getResourceClass()).get().sendRequest(); + ServerResponse> response = scimRequestBuilder.list(adapter.getResourceClass(), adapter.getSCIMEndpoint()).get().sendRequest(); ListResponse resourceTypeListResponse = response.getResource(); for (var resource : resourceTypeListResponse.getListedResources()) { @@ -287,9 +277,7 @@ public class ScimClient { case "DELETE_REMOTE": LOGGER.info("Delete remote resource"); scimRequestBuilder - .delete(genScimUrl(adapter.getSCIMEndpoint(), - resource.getId().get()), - adapter.getResourceClass()) + .delete(adapter.getResourceClass(), adapter.getSCIMEndpoint(), resource.getId().get()) .sendRequest(); syncRes.increaseRemoved(); break; diff --git a/src/main/java/sh/libre/scim/core/ScimDispatcher.java b/src/main/java/sh/libre/scim/core/ScimDispatcher.java index f76102cafa..293106b030 100644 --- a/src/main/java/sh/libre/scim/core/ScimDispatcher.java +++ b/src/main/java/sh/libre/scim/core/ScimDispatcher.java @@ -1,6 +1,8 @@ package sh.libre.scim.core; import java.util.function.Consumer; + +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; @@ -21,7 +23,8 @@ public class ScimDispatcher { public void run(String scope, Consumer f) { session.getContext().getRealm().getComponentsStream() .filter((m) -> { - return ScimStorageProviderFactory.ID.equals(m.getProviderId()) && m.get("enabled", true) + return StringUtils.equals(ScimStorageProviderFactory.ID, m.getProviderId()) + && m.get("enabled", true) && m.get("propagation-" + scope, false); }) .forEach(m -> runOne(m, f)); diff --git a/src/main/java/sh/libre/scim/core/UserAdapter.java b/src/main/java/sh/libre/scim/core/UserAdapter.java index b2283e59fd..ff85170bc0 100644 --- a/src/main/java/sh/libre/scim/core/UserAdapter.java +++ b/src/main/java/sh/libre/scim/core/UserAdapter.java @@ -3,8 +3,10 @@ package sh.libre.scim.core; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import de.captaingoldfish.scim.sdk.common.resources.User; @@ -13,8 +15,7 @@ import de.captaingoldfish.scim.sdk.common.resources.complex.Name; import de.captaingoldfish.scim.sdk.common.resources.multicomplex.PersonRole; import de.captaingoldfish.scim.sdk.common.resources.complex.Meta; - -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; @@ -26,6 +27,8 @@ public class UserAdapter extends Adapter { private String email; private Boolean active; private String[] roles; + private String firstName; + private String lastName; public UserAdapter(KeycloakSession session, String componentId) { super(session, componentId, "User", Logger.getLogger(UserAdapter.class)); @@ -79,6 +82,22 @@ public class UserAdapter extends Adapter { this.roles = roles; } + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + @Override public Class getResourceClass() { return User.class; @@ -96,19 +115,17 @@ public class UserAdapter extends Adapter { setDisplayName(displayName); setEmail(user.getEmail()); setActive(user.isEnabled()); + setFirstName(user.getFirstName()); + setLastName(user.getLastName()); var rolesSet = new HashSet(); user.getGroupsStream().flatMap(g -> g.getRoleMappingsStream()) - .filter((r) -> r.getFirstAttribute("scim").equals("true")).map((r) -> r.getName()) + .filter((r) -> StringUtils.equals(r.getFirstAttribute("scim"), "true")) + .map((r) -> r.getName()) + .forEach(r -> rolesSet.add(r)); + user.getRoleMappingsStream() + .filter((r) -> StringUtils.equals(r.getFirstAttribute("scim"), "true")) + .map((r) -> r.getName()) .forEach(r -> rolesSet.add(r)); - - user.getRoleMappingsStream().filter((r) -> { - var attr = r.getFirstAttribute("scim"); - if (attr == null) { - return false; - } - return attr.equals("true"); - }).map((r) -> r.getName()).forEach(r -> rolesSet.add(r)); - var roles = new String[rolesSet.size()]; rolesSet.toArray(roles); setRoles(roles); @@ -127,30 +144,31 @@ public class UserAdapter extends Adapter { } @Override - public User toSCIM(Boolean addMeta) { + public User toSCIM() { var user = new User(); user.setExternalId(id); user.setUserName(username); user.setId(externalId); user.setDisplayName(displayName); Name name = new Name(); + name.setFamilyName(lastName); + name.setGivenName(firstName); user.setName(name); var emails = new ArrayList(); if (email != null) { emails.add( - Email.builder().value(getEmail()).build()); + Email.builder().value(getEmail()).build()); } user.setEmails(emails); user.setActive(active); - if (addMeta) { - var meta = new Meta(); - try { - var uri = new URI("Users/" + externalId); - meta.setLocation(uri.toString()); - } catch (URISyntaxException e) { - } - user.setMeta(meta); + var meta = new Meta(); + try { + var uri = new URI("Users/" + externalId); + meta.setLocation(uri.toString()); + } catch (URISyntaxException e) { + LOGGER.warn(e); } + user.setMeta(meta); List roles = new ArrayList(); for (var r : this.roles) { var role = new PersonRole(); @@ -212,11 +230,13 @@ public class UserAdapter extends Adapter { @Override public Stream getResourceStream() { - return this.session.users().getUsersStream(this.session.getContext().getRealm()); + Map params = new HashMap() { + }; + return this.session.users().searchForUserStream(realm, params); } @Override public Boolean skipRefresh() { - return getUsername().equals("admin"); + return StringUtils.equals(getUsername(), "admin"); } } diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java index 3fbad121ca..24dab78dbb 100644 --- a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java +++ b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java @@ -1,8 +1,9 @@ package sh.libre.scim.event; import java.util.HashMap; -import java.util.regex.*; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; @@ -99,7 +100,7 @@ public class ScimEventListenerProvider implements EventListenerProvider { var groupId = matcher.group(2); LOGGER.infof("%s %s from %s", event.getOperationType(), userId, groupId); var group = getGroup(groupId); - dispatcher.run(ScimDispatcher.SCOPE_GROUP, (client) -> client.replace(GroupAdapter.class, group)); + group.setSingleAttribute("scim-dirty", "true"); var user = getUser(userId); dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user)); } @@ -107,10 +108,10 @@ public class ScimEventListenerProvider implements EventListenerProvider { var type = matcher.group(1); var id = matcher.group(2); LOGGER.infof("%s %s %s roles", event.getOperationType(), type, id); - if (type.equals("users")) { + if (StringUtils.equals(type, "users")) { var user = getUser(id); dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user)); - } else if (type.equals("groups")) { + } else if (StringUtils.equals(type, "groups")) { var group = getGroup(id); session.users().getGroupMembersStream(session.getContext().getRealm(), group).forEach(user -> { dispatcher.run(ScimDispatcher.SCOPE_USER, (client) -> client.replace(UserAdapter.class, user)); diff --git a/src/main/java/sh/libre/scim/jpa/ScimResource.java b/src/main/java/sh/libre/scim/jpa/ScimResource.java index a9f69581cd..62dbdf0ee5 100644 --- a/src/main/java/sh/libre/scim/jpa/ScimResource.java +++ b/src/main/java/sh/libre/scim/jpa/ScimResource.java @@ -1,12 +1,12 @@ 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; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.Table; @Entity @IdClass(ScimResourceId.class) diff --git a/src/main/java/sh/libre/scim/jpa/ScimResourceId.java b/src/main/java/sh/libre/scim/jpa/ScimResourceId.java index 7775543a54..1fa0d21fcd 100644 --- a/src/main/java/sh/libre/scim/jpa/ScimResourceId.java +++ b/src/main/java/sh/libre/scim/jpa/ScimResourceId.java @@ -68,6 +68,7 @@ public class ScimResourceId implements Serializable { if (!(other instanceof ScimResourceId)) return false; var o = (ScimResourceId) other; + // TODO return (o.id == id && o.realmId == realmId && o.componentId == componentId && diff --git a/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java b/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java index 34ea984843..265c6c65ea 100644 --- a/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java +++ b/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java @@ -3,8 +3,9 @@ package sh.libre.scim.storage; import java.util.Date; import java.util.List; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MediaType; +import org.apache.commons.lang3.StringUtils; import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; @@ -17,6 +18,7 @@ import org.keycloak.storage.UserStorageProviderFactory; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.user.ImportSynchronization; import org.keycloak.storage.user.SynchronizationResult; +import org.keycloak.timer.TimerProvider; import sh.libre.scim.core.GroupAdapter; import sh.libre.scim.core.ScimDispatcher; @@ -34,6 +36,7 @@ public class ScimStorageProviderFactory .property() .name("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)") @@ -70,12 +73,14 @@ public class ScimStorageProviderFactory .name("propagation-user") .type(ProviderConfigProperty.BOOLEAN_TYPE) .label("Enable user propagation") + .helpText("Should operation on users be propagated to this provider ?") .defaultValue("true") .add() .property() .name("propagation-group") .type(ProviderConfigProperty.BOOLEAN_TYPE) .label("Enable group propagation") + .helpText("Should operation on groups be propagated to this provider ?") .defaultValue("true") .add() .property() @@ -127,10 +132,10 @@ public class ScimStorageProviderFactory var realm = session.realms().getRealm(realmId); session.getContext().setRealm(realm); var dispatcher = new ScimDispatcher(session); - if (model.get("propagation-user").equals("true")) { + if (StringUtils.equals(model.get("propagation-user"), "true")) { dispatcher.runOne(model, (client) -> client.sync(UserAdapter.class, result)); } - if (model.get("propagation-group").equals("true")) { + if (StringUtils.equals(model.get("propagation-group"), "true")) { dispatcher.runOne(model, (client) -> client.sync(GroupAdapter.class, result)); } } @@ -147,4 +152,27 @@ public class ScimStorageProviderFactory return this.sync(sessionFactory, realmId, model); } + @Override + public void postInit(KeycloakSessionFactory factory) { + var timer = factory.create().getProvider(TimerProvider.class); + timer.scheduleTask(taskSession -> { + for (var realm : taskSession.realms().getRealmsStream().toList()) { + KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() { + @Override + public void run(KeycloakSession session) { + session.getContext().setRealm(realm); + var dispatcher = new ScimDispatcher(session); + for (var group : session.groups().getGroupsStream(realm) + .filter(x -> StringUtils.equals(x.getFirstAttribute("scim-dirty"), "true")).toList()) { + LOGGER.debug(group.getName() + " is dirty"); + dispatcher.run(ScimDispatcher.SCOPE_GROUP, + (client) -> client.replace(GroupAdapter.class, group)); + group.removeAttribute("scim-dirty"); + } + } + + }); + } + }, 30000, "scim-background"); + } }