From 50029c254e22dfdf31879359f6385683d116f523 Mon Sep 17 00:00:00 2001
From: Hugo Renard <hugo.renard@protonmail.com>
Date: Wed, 23 Feb 2022 18:14:35 +0100
Subject: [PATCH] add group + group sync support

---
 src/main/java/sh/libre/scim/core/Adapter.java |  31 ++-
 .../java/sh/libre/scim/core/GroupAdapter.java | 184 ++++++++++++++++++
 .../java/sh/libre/scim/core/ScimClient.java   |  70 ++++---
 .../java/sh/libre/scim/core/UserAdapter.java  |  37 ++--
 .../scim/event/ScimEventListenerProvider.java |  38 +++-
 .../storage/ScimStorageProviderFactory.java   |   5 +-
 6 files changed, 309 insertions(+), 56 deletions(-)
 create mode 100644 src/main/java/sh/libre/scim/core/GroupAdapter.java

diff --git a/src/main/java/sh/libre/scim/core/Adapter.java b/src/main/java/sh/libre/scim/core/Adapter.java
index 6318ebc..2f8f40c 100644
--- a/src/main/java/sh/libre/scim/core/Adapter.java
+++ b/src/main/java/sh/libre/scim/core/Adapter.java
@@ -1,10 +1,14 @@
 package sh.libre.scim.core;
 
+import java.util.stream.Stream;
+
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
 import javax.ws.rs.NotFoundException;
 
 import org.jboss.logging.Logger;
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RoleMapperModel;
 
 import sh.libre.scim.jpa.ScimResource;
@@ -16,14 +20,16 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
     protected final String type;
     protected final String componentId;
     protected final EntityManager em;
+    protected final KeycloakSession session;
 
     protected String id;
     protected String externalId;
 
-    public Adapter(String realmId, String componentId, EntityManager em, String type, Logger logger) {
-        this.realmId = realmId;
+    public Adapter(KeycloakSession session, String componentId, String type, Logger logger) {
+        this.session = session;
+        this.realmId = session.getContext().getRealm().getId();
         this.componentId = componentId;
-        this.em = em;
+        this.em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
         this.type = type;
         this.LOGGER = logger;
     }
@@ -63,6 +69,10 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
     }
 
     public TypedQuery<ScimResource> query(String query, String id) {
+        return query(query, id, type);
+    }
+
+    public TypedQuery<ScimResource> query(String query, String id, String type) {
         return this.em
                 .createNamedQuery(query, ScimResource.class)
                 .setParameter("type", type)
@@ -92,14 +102,20 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
     }
 
     public void deleteMapping() {
-        this.em.remove(this.toMapping());
+        var mapping = this.em.merge(toMapping());
+        this.em.remove(mapping);
+    }
+
+    public void apply(ScimResource mapping) {
+        setId(mapping.getId());
+        setExternalId(mapping.getExternalId());
     }
 
     public abstract void apply(M model);
 
     public abstract void apply(S resource);
 
-    public abstract void apply(ScimResource resource);
+    public abstract Class<S> getResourceClass();
 
     public abstract S toSCIM(Boolean addMeta);
 
@@ -107,4 +123,9 @@ public abstract class Adapter<M extends RoleMapperModel, S extends com.unboundid
 
     public abstract Boolean tryToMap();
 
+    public abstract void createEntity();
+    
+    public abstract Stream<M> getResourceStream();
+
+    public abstract Boolean skipRefresh();
 }
diff --git a/src/main/java/sh/libre/scim/core/GroupAdapter.java b/src/main/java/sh/libre/scim/core/GroupAdapter.java
new file mode 100644
index 0000000..0e4b84f
--- /dev/null
+++ b/src/main/java/sh/libre/scim/core/GroupAdapter.java
@@ -0,0 +1,184 @@
+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 com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.unboundid.scim2.common.types.GroupResource;
+import com.unboundid.scim2.common.types.Member;
+import com.unboundid.scim2.common.types.Meta;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.jpa.entities.GroupEntity;
+import org.keycloak.models.jpa.entities.UserEntity;
+import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+public class GroupAdapter extends Adapter<GroupModel, GroupResource> {
+
+    private String displayName;
+    private Set<String> members = new HashSet<String>();
+
+    public GroupAdapter(KeycloakSession session, String componentId) {
+        super(session, componentId, "Group", Logger.getLogger(GroupAdapter.class));
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        if (this.displayName == null) {
+            this.displayName = displayName;
+        }
+    }
+
+    @Override
+    public Class<GroupResource> getResourceClass() {
+        return GroupResource.class;
+    }
+
+    @Override
+    public void apply(GroupModel group) {
+        setId(group.getId());
+        setDisplayName(group.getName());
+        this.members = session.users()
+                .getGroupMembersStream(session.getContext().getRealm(), group)
+                .map(x -> x.getId())
+                .collect(Collectors.toSet());
+        ObjectMapper Obj = new ObjectMapper();
+        try {
+            String jsonStr = Obj.writerWithDefaultPrettyPrinter().writeValueAsString(this.members);
+            LOGGER.info(jsonStr);
+        } catch (JsonProcessingException e) {
+        }
+    }
+
+    @Override
+    public void apply(GroupResource group) {
+        setExternalId(group.getId());
+        setDisplayName(group.getDisplayName());
+        var groupMembers = group.getMembers();
+        if (groupMembers != null && groupMembers.size() > 0) {
+            this.members = new HashSet<String>();
+            for (var groupMember : groupMembers) {
+                var userMapping = this.query("findByExternalId", groupMember.getValue(), "User")
+                        .getSingleResult();
+                this.members.add(userMapping.getId());
+            }
+        }
+    }
+
+    @Override
+    public GroupResource toSCIM(Boolean addMeta) {
+        var group = new GroupResource();
+        group.setId(externalId);
+        group.setExternalId(id);
+        group.setDisplayName(displayName);
+        if (members.size() > 0) {
+            var groupMembers = new ArrayList<Member>();
+            for (var member : members) {
+                var groupMember = new Member();
+                try {
+                    var userMapping = this.query("findById", member, "User").getSingleResult();
+                    groupMember.setValue(userMapping.getExternalId());
+                    var ref = new URI(String.format("Users/%s", userMapping.getExternalId()));
+                    groupMember.setRef(ref);
+                    groupMembers.add(groupMember);
+                } catch (Exception e) {
+                    LOGGER.error(e);
+                }
+            }
+            group.setMembers(groupMembers);
+        }
+        if (addMeta) {
+            var meta = new Meta();
+            try {
+                var uri = new URI("Groups/" + externalId);
+                meta.setLocation(uri);
+            } catch (URISyntaxException e) {
+            }
+            group.setMeta(meta);
+        }
+        ObjectMapper Obj = new ObjectMapper();
+        try {
+            String jsonStr = Obj.writerWithDefaultPrettyPrinter().writeValueAsString(group);
+            LOGGER.info(jsonStr);
+        } catch (JsonProcessingException e) {
+        }
+        return group;
+    }
+
+    @Override
+    public Boolean entityExists() {
+        if (this.id == null) {
+            return false;
+        }
+        var group = this.em.find(GroupEntity.class, this.id);
+        if (group != null) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public Boolean tryToMap() {
+        try {
+            var groupEntity = this.em
+                    .createQuery("select g from GroupEntity g where g.name=:name",
+                            GroupEntity.class)
+                    .setParameter("name", displayName)
+                    .getSingleResult();
+            setId(groupEntity.getId());
+            return true;
+        } catch (Exception e) {
+        }
+        return false;
+    }
+
+    @Override
+    public void createEntity() {
+        var kcGroup = new GroupEntity();
+        kcGroup.setId(KeycloakModelUtils.generateId());
+        kcGroup.setRealm(realmId);
+        kcGroup.setName(displayName);
+        kcGroup.setParentId(GroupEntity.TOP_PARENT_ID);
+        this.em.persist(kcGroup);
+        this.id = kcGroup.getId();
+        for (String mId : members) {
+            try {
+                var user = this.em.find(UserEntity.class, mId);
+                if (user == null) {
+                    throw new NoResultException();
+                }
+                var membership = new UserGroupMembershipEntity();
+                membership.setUser(user);
+                membership.setGroupId(kcGroup.getId());
+                this.em.persist(membership);
+            } catch (Exception e) {
+                LOGGER.warn(e);
+            }
+        }
+    }
+
+    @Override
+    public Stream<GroupModel> getResourceStream() {
+        return this.session.groups().getGroupsStream(this.session.getContext().getRealm());
+    }
+
+    @Override
+    public Boolean skipRefresh() {
+        return false;
+    }
+
+}
diff --git a/src/main/java/sh/libre/scim/core/ScimClient.java b/src/main/java/sh/libre/scim/core/ScimClient.java
index 66e71d5..c35d296 100644
--- a/src/main/java/sh/libre/scim/core/ScimClient.java
+++ b/src/main/java/sh/libre/scim/core/ScimClient.java
@@ -7,7 +7,6 @@ import javax.ws.rs.client.Client;
 import com.unboundid.scim2.client.ScimService;
 import com.unboundid.scim2.common.ScimResource;
 import com.unboundid.scim2.common.exceptions.ScimException;
-import com.unboundid.scim2.common.types.UserResource;
 
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
@@ -17,7 +16,6 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RoleMapperModel;
 import org.keycloak.storage.user.SynchronizationResult;
 
-import io.github.resilience4j.core.IntervalFunction;
 import io.github.resilience4j.retry.RetryConfig;
 import io.github.resilience4j.retry.RetryRegistry;
 
@@ -60,8 +58,8 @@ public class ScimClient {
     protected <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> A getAdapter(
             Class<A> aClass) {
         try {
-            return aClass.getDeclaredConstructor(String.class, String.class, EntityManager.class)
-                    .newInstance(getRealmId(), this.model.getId(), getEM());
+            return aClass.getDeclaredConstructor(KeycloakSession.class, String.class)
+                    .newInstance(session, this.model.getId());
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -72,16 +70,18 @@ public class ScimClient {
         var adapter = getAdapter(aClass);
         adapter.apply(kcModel);
         var retry = registry.retry("create-" + adapter.getId());
-        var spUser = retry.executeSupplier(() -> {
+        var resource = retry.executeSupplier(() -> {
             try {
-                return scimService.createRequest(adapter.getSCIMEndpoint(), adapter.toSCIM(false))
+                return scimService.createRequest(adapter.getSCIMEndpoint(),
+                        adapter.toSCIM(false))
                         .contentType(contentType).invoke();
             } catch (ScimException e) {
                 throw new RuntimeException(e);
             }
         });
-        adapter.apply(spUser);
+        adapter.apply(resource);
         adapter.saveMapping();
+
     };
 
     public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void replace(Class<A> aClass,
@@ -129,35 +129,41 @@ public class ScimClient {
         }
     }
 
-    public void refreshUsers(SynchronizationResult syncRes) {
-        LOGGER.info("Refresh Users");
-        this.session.users().getUsersStream(this.session.getContext().getRealm()).forEach(kcUser -> {
-            LOGGER.infof("Reconciling local user %s", kcUser.getId());
-            if (!kcUser.getUsername().equals("admin")) {
-                var adapter = getAdapter(UserAdapter.class);
-                adapter.apply(kcUser);
+    public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void refreshResources(
+            Class<A> aClass,
+            SynchronizationResult syncRes) {
+        LOGGER.info("Refresh resources");
+        getAdapter(aClass).getResourceStream().forEach(resource -> {
+            var adapter = getAdapter(aClass);
+            adapter.apply(resource);
+            LOGGER.infof("Reconciling local resource %s", adapter.getId());
+            if (!adapter.skipRefresh()) {
                 var mapping = adapter.getMapping();
                 if (mapping == null) {
                     LOGGER.info("Creating it");
-                    this.create(UserAdapter.class, kcUser);
+                    this.create(aClass, resource);
                 } else {
                     LOGGER.info("Replacing it");
-                    this.replace(UserAdapter.class, kcUser);
+                    this.replace(aClass, resource);
                 }
                 syncRes.increaseUpdated();
             }
         });
+
     }
 
-    public void importUsers(SynchronizationResult syncRes) {
-        LOGGER.info("Import Users");
+    public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void importResources(
+            Class<A> aClass, SynchronizationResult syncRes) {
+        LOGGER.info("Import");
         try {
-            var spUsers = scimService.searchRequest("Users").contentType(contentType).invoke(UserResource.class);
-            for (var spUser : spUsers) {
+            var adapter = getAdapter(aClass);
+            var resources = scimService.searchRequest(adapter.getSCIMEndpoint()).contentType(contentType)
+                    .invoke(adapter.getResourceClass());
+            for (var resource : resources) {
                 try {
-                    LOGGER.infof("Reconciling remote user %s", spUser.getId());
-                    var adapter = getAdapter(UserAdapter.class);
-                    adapter.apply(spUser);
+                    LOGGER.infof("Reconciling remote resource %s", resource.getId());
+                    adapter = getAdapter(aClass);
+                    adapter.apply(resource);
 
                     var mapping = adapter.getMapping();
                     if (mapping != null) {
@@ -173,25 +179,28 @@ public class ScimClient {
 
                     var mapped = adapter.tryToMap();
                     if (mapped) {
-                        LOGGER.info("Matched a user");
+                        LOGGER.info("Matched");
                         adapter.saveMapping();
                     } else {
                         switch (this.model.get("sync-import-action")) {
                             case "CREATE_LOCAL":
-                                LOGGER.info("Create local user");
+                                LOGGER.info("Create local resource");
                                 adapter.createEntity();
                                 adapter.saveMapping();
                                 syncRes.increaseAdded();
                                 break;
                             case "DELETE_REMOTE":
-                                LOGGER.info("Delete remote user");
-                                scimService.deleteRequest("Users", spUser.getId()).contentType(contentType)
+                                LOGGER.info("Delete remote resource");
+                                scimService.deleteRequest(adapter.getSCIMEndpoint(), resource.getId())
+                                        .contentType(contentType)
                                         .invoke();
                                 syncRes.increaseRemoved();
                                 break;
                         }
                     }
                 } catch (Exception e) {
+                    LOGGER.error(e);
+                    e.printStackTrace();
                     syncRes.increaseFailed();
                 }
             }
@@ -200,12 +209,13 @@ public class ScimClient {
         }
     }
 
-    public void sync(SynchronizationResult syncRes) {
+    public <M extends RoleMapperModel, S extends ScimResource, A extends Adapter<M, S>> void sync(Class<A> aClass,
+            SynchronizationResult syncRes) {
         if (this.model.get("sync-import", false)) {
-            this.importUsers(syncRes);
+            this.importResources(aClass, syncRes);
         }
         if (this.model.get("sync-refresh", false)) {
-            this.refreshUsers(syncRes);
+            this.refreshResources(aClass, syncRes);
         }
     }
 
diff --git a/src/main/java/sh/libre/scim/core/UserAdapter.java b/src/main/java/sh/libre/scim/core/UserAdapter.java
index 0ed5328..11c2472 100644
--- a/src/main/java/sh/libre/scim/core/UserAdapter.java
+++ b/src/main/java/sh/libre/scim/core/UserAdapter.java
@@ -3,20 +3,18 @@ package sh.libre.scim.core;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
-
-import javax.persistence.EntityManager;
+import java.util.stream.Stream;
 
 import com.unboundid.scim2.common.types.Email;
 import com.unboundid.scim2.common.types.Meta;
 import com.unboundid.scim2.common.types.UserResource;
 
 import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.jpa.entities.UserEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
-import sh.libre.scim.jpa.ScimResource;
-
 public class UserAdapter extends Adapter<UserModel, UserResource> {
 
     private String username;
@@ -24,8 +22,8 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
     private String email;
     private Boolean active;
 
-    public UserAdapter(String realmId, String componentId, EntityManager em) {
-        super(realmId, componentId, em, "User", Logger.getLogger(UserAdapter.class));
+    public UserAdapter(KeycloakSession session, String componentId) {
+        super(session, componentId, "User", Logger.getLogger(UserAdapter.class));
     }
 
     public String getUsername() {
@@ -68,6 +66,11 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
         }
     }
 
+    @Override
+    public Class<UserResource> getResourceClass() {
+        return UserResource.class;
+    }
+
     @Override
     public void apply(UserModel user) {
         setId(user.getId());
@@ -94,12 +97,6 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
         }
     }
 
-    @Override
-    public void apply(ScimResource mapping) {
-        setId(mapping.getId());
-        setExternalId(mapping.getExternalId());
-    }
-
     @Override
     public UserResource toSCIM(Boolean addMeta) {
         var user = new UserResource();
@@ -126,7 +123,8 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
         return user;
     }
 
-    public UserEntity createEntity() {
+    @Override
+    public void createEntity() {
         var kcUser = new UserEntity();
         kcUser.setId(KeycloakModelUtils.generateId());
         kcUser.setRealmId(realmId);
@@ -134,9 +132,9 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
         kcUser.setEmail(email, false);
         this.em.persist(kcUser);
         this.id = kcUser.getId();
-        return kcUser;
     }
 
+    @Override
     public Boolean entityExists() {
         if (this.id == null) {
             return false;
@@ -148,6 +146,7 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
         return false;
     }
 
+    @Override
     public Boolean tryToMap() {
         try {
             var userEntity = this.em
@@ -163,4 +162,14 @@ public class UserAdapter extends Adapter<UserModel, UserResource> {
         }
         return false;
     }
+
+    @Override
+    public Stream<UserModel> getResourceStream() {
+        return this.session.users().getUsersStream(this.session.getContext().getRealm());
+    }
+
+    @Override
+    public Boolean skipRefresh() {
+        return getUsername().equals("admin");
+    }
 }
diff --git a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java
index a64c823..13ccbad 100644
--- a/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java
+++ b/src/main/java/sh/libre/scim/event/ScimEventListenerProvider.java
@@ -1,5 +1,8 @@
 package sh.libre.scim.event;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
 import org.jboss.logging.Logger;
 import org.keycloak.events.Event;
 import org.keycloak.events.EventListenerProvider;
@@ -7,9 +10,12 @@ 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.GroupModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.GroupRepresentation;
 
+import sh.libre.scim.core.GroupAdapter;
 import sh.libre.scim.core.ScimDispatcher;
 import sh.libre.scim.core.UserAdapter;
 
@@ -48,7 +54,6 @@ public class ScimEventListenerProvider implements EventListenerProvider {
             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.create(UserAdapter.class, user));
             }
@@ -60,11 +65,28 @@ public class ScimEventListenerProvider implements EventListenerProvider {
                 dispatcher.run((client) -> client.delete(UserAdapter.class, userId));
             }
         }
-        if (event.getResourceType() == ResourceType.COMPONENT) {
-            if (event.getOperationType() == OperationType.CREATE
-                    || (event.getOperationType() == OperationType.UPDATE)) {
-                LOGGER.infof("%s %s", event.getResourcePath(), event.getOperationType());
-                // dispatcher.run((client) -> client.syncUsers());
+        if (event.getResourceType() == ResourceType.GROUP) {
+            var groupId = event.getResourcePath().replace("groups/", "");
+            LOGGER.infof("%s %s", event.getResourcePath(), event.getOperationType());
+            if (event.getOperationType() == OperationType.CREATE) {
+                var group = getGroup(groupId);
+                dispatcher.run((client) -> client.create(GroupAdapter.class, group));
+            }
+            if (event.getOperationType() == OperationType.UPDATE) {
+                var group = getGroup(groupId);
+                dispatcher.run((client) -> client.replace(GroupAdapter.class, group));
+            }
+            if (event.getOperationType() == OperationType.DELETE) {
+                dispatcher.run((client) -> client.delete(GroupAdapter.class, groupId));
+            }
+        }
+        if (event.getResourceType() == ResourceType.GROUP_MEMBERSHIP) {
+            ObjectMapper obj = new ObjectMapper();
+            try {
+                var groupRepresentation = obj.readValue(event.getRepresentation(), GroupRepresentation.class);
+                var group = getGroup(groupRepresentation.getId());
+                dispatcher.run((client) -> client.replace(GroupAdapter.class, group));
+            } catch (JsonProcessingException e) {
             }
         }
     }
@@ -72,4 +94,8 @@ public class ScimEventListenerProvider implements EventListenerProvider {
     private UserModel getUser(String id) {
         return session.users().getUserById(session.getContext().getRealm(), id);
     }
+
+    private GroupModel getGroup(String id) {
+        return session.groups().getGroupById(session.getContext().getRealm(), id);
+    }
 }
diff --git a/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java b/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java
index f1bc82d..081a1c6 100644
--- a/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java
+++ b/src/main/java/sh/libre/scim/storage/ScimStorageProviderFactory.java
@@ -21,7 +21,9 @@ import org.keycloak.storage.UserStorageProviderModel;
 import org.keycloak.storage.user.ImportSynchronization;
 import org.keycloak.storage.user.SynchronizationResult;
 
+import sh.libre.scim.core.GroupAdapter;
 import sh.libre.scim.core.ScimClient;
+import sh.libre.scim.core.UserAdapter;
 
 public class ScimStorageProviderFactory
         implements UserStorageProviderFactory<ScimStorageProvider>, ImportSynchronization {
@@ -110,7 +112,8 @@ public class ScimStorageProviderFactory
                 var client = new ScimClient(model, session);
                 model.setEnabled(false);
                 realm.updateComponent(model);
-                client.sync(result);
+                client.sync(UserAdapter.class, result);
+                client.sync(GroupAdapter.class, result);
                 client.close();
                 model.setEnabled(true);
                 realm.updateComponent(model);
-- 
GitLab