keycloak-developers

Details

diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java
index 7807094..ce6db74 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java
@@ -20,12 +20,7 @@ package org.keycloak.admin.client.resource;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.keycloak.representations.idm.GroupRepresentation;
 
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
+import javax.ws.rs.*;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.util.List;
@@ -35,12 +30,44 @@ import java.util.List;
  * @version $Revision: 1 $
  */
 public interface GroupsResource {
+
+    /**
+     * Get all groups.
+     * @return A list containing all groups.
+     */
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     List<GroupRepresentation> groups();
 
     /**
+     * Get groups by pagination params.
+     * @param first index of the first element
+     * @param max max number of occurrences
+     * @return A list containing the slice of all groups.
+     */
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    List<GroupRepresentation> groups(@QueryParam("first") Integer first, @QueryParam("max") Integer max);
+
+    /**
+     * Get groups by pagination params.
+     * @param search max number of occurrences
+     * @param first index of the first element
+     * @param max max number of occurrences
+     * @return A list containing the slice of all groups.
+     */
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    List<GroupRepresentation> groups(@QueryParam("search") String search,
+                                     @QueryParam("first") Integer first,
+                                     @QueryParam("max") Integer max);
+
+    /**
      * create or add a top level realm groupSet or create child.  This will update the group and set the parent if it exists.  Create it and set the parent
      * if the group doesn't exist.
      *
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 0bed826..d396463 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -20,32 +20,12 @@ package org.keycloak.models.cache.infinispan;
 import org.keycloak.Config;
 import org.keycloak.common.enums.SslRequired;
 import org.keycloak.component.ComponentModel;
-import org.keycloak.models.AuthenticationExecutionModel;
-import org.keycloak.models.AuthenticationFlowModel;
-import org.keycloak.models.AuthenticatorConfigModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientTemplateModel;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.IdentityProviderMapperModel;
-import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.PasswordPolicy;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RequiredActionProviderModel;
-import org.keycloak.models.RequiredCredentialModel;
-import org.keycloak.models.RoleModel;
+import org.keycloak.models.*;
 import org.keycloak.models.cache.CachedRealmModel;
 import org.keycloak.models.cache.infinispan.entities.CachedRealm;
 import org.keycloak.storage.UserStorageProvider;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -328,7 +308,7 @@ public class RealmAdapter implements CachedRealmModel {
         getDelegateForUpdate();
         updated.setLoginWithEmailAllowed(loginWithEmailAllowed);
     }
-    
+
     @Override
     public boolean isDuplicateEmailsAllowed() {
         if (isUpdated()) return updated.isDuplicateEmailsAllowed();
@@ -797,9 +777,9 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public void setEnabledEventTypes(Set<String> enabledEventTypes) {
         getDelegateForUpdate();
-        updated.setEnabledEventTypes(enabledEventTypes);        
+        updated.setEnabledEventTypes(enabledEventTypes);
     }
-    
+
     @Override
     public boolean isAdminEventsEnabled() {
         if (isUpdated()) return updated.isAdminEventsEnabled();
@@ -823,7 +803,7 @@ public class RealmAdapter implements CachedRealmModel {
         getDelegateForUpdate();
         updated.setAdminEventsDetailsEnabled(enabled);
     }
-    
+
     @Override
     public ClientModel getMasterAdminClient() {
         return cached.getMasterAdminClient()==null ? null : cacheSession.getRealm(Config.getAdminRealm()).getClientById(cached.getMasterAdminClient());
@@ -1226,6 +1206,16 @@ public class RealmAdapter implements CachedRealmModel {
     }
 
     @Override
+    public List<GroupModel> getTopLevelGroups(Integer first, Integer max) {
+        return cacheSession.getTopLevelGroups(this, first, max);
+    }
+
+    @Override
+    public List<GroupModel> searchForGroupByName(String search, Integer first, Integer max) {
+        return cacheSession.searchForGroupByName(this, search, first, max);
+    }
+
+    @Override
     public boolean removeGroup(GroupModel group) {
         return cacheSession.removeGroup(this, group);
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 52c1309..47267ad 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -19,50 +19,15 @@ package org.keycloak.models.cache.infinispan;
 
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
-import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.migration.MigrationModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientTemplateModel;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakTransaction;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RealmProvider;
-import org.keycloak.models.RoleModel;
+import org.keycloak.models.*;
 import org.keycloak.models.cache.CacheRealmProvider;
 import org.keycloak.models.cache.CachedRealmModel;
-import org.keycloak.models.cache.infinispan.entities.CachedClient;
-import org.keycloak.models.cache.infinispan.entities.CachedClientRole;
-import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate;
-import org.keycloak.models.cache.infinispan.entities.CachedGroup;
-import org.keycloak.models.cache.infinispan.entities.CachedRealm;
-import org.keycloak.models.cache.infinispan.entities.CachedRealmRole;
-import org.keycloak.models.cache.infinispan.entities.CachedRole;
-import org.keycloak.models.cache.infinispan.entities.ClientListQuery;
-import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
-import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
-import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
-import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
-import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent;
-import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent;
-import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent;
-import org.keycloak.models.cache.infinispan.events.GroupAddedEvent;
-import org.keycloak.models.cache.infinispan.events.GroupMovedEvent;
-import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent;
-import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent;
-import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
-import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
-import org.keycloak.models.cache.infinispan.events.RoleAddedEvent;
-import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent;
-import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent;
+import org.keycloak.models.cache.infinispan.entities.*;
+import org.keycloak.models.cache.infinispan.events.*;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 
 /**
@@ -913,6 +878,47 @@ public class RealmCacheSession implements CacheRealmProvider {
     }
 
     @Override
+    public List<GroupModel> getTopLevelGroups(RealmModel realm, Integer first, Integer max) {
+        String cacheKey = getTopGroupsQueryCacheKey(realm.getId() + first + max);
+        boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId() + first + max);
+        if (queryDB) {
+            return getDelegate().getTopLevelGroups(realm, first, max);
+        }
+
+        GroupListQuery query = cache.get(cacheKey, GroupListQuery.class);
+        if (Objects.nonNull(query)) {
+            logger.tracev("getTopLevelGroups cache hit: {0}", realm.getName());
+        }
+
+        if (Objects.isNull(query)) {
+            Long loaded = cache.getCurrentRevision(cacheKey);
+            List<GroupModel> model = getDelegate().getTopLevelGroups(realm, first, max);
+            if (model == null) return null;
+            Set<String> ids = new HashSet<>();
+            for (GroupModel client : model) ids.add(client.getId());
+            query = new GroupListQuery(loaded, cacheKey, realm, ids);
+            logger.tracev("adding realm getTopLevelGroups cache miss: realm {0} key {1}", realm.getName(), cacheKey);
+            cache.addRevisioned(query, startupRevision);
+            return model;
+        }
+        List<GroupModel> list = new LinkedList<>();
+        for (String id : query.getGroups()) {
+            GroupModel group = session.realms().getGroupById(id, realm);
+            if (Objects.isNull(group)) {
+                invalidations.add(cacheKey);
+                return getDelegate().getTopLevelGroups(realm);
+            }
+            list.add(group);
+        }
+        return list;
+    }
+
+    @Override
+    public List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
+        return getDelegate().searchForGroupByName(realm, search, first, max);
+    }
+
+    @Override
     public boolean removeGroup(RealmModel realm, GroupModel group) {
         invalidateGroup(group.getId(), realm.getId(), true);
         listInvalidations.add(realm.getId());
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
index 98b130a..77bc44d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
@@ -17,19 +17,7 @@
 
 package org.keycloak.models.jpa.entities;
 
-import javax.persistence.Access;
-import javax.persistence.AccessType;
-import javax.persistence.CascadeType;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.FetchType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.NamedQueries;
-import javax.persistence.NamedQuery;
-import javax.persistence.OneToMany;
-import javax.persistence.Table;
+import javax.persistence.*;
 import java.util.ArrayList;
 import java.util.Collection;
 
@@ -39,6 +27,8 @@ import java.util.Collection;
  */
 @NamedQueries({
         @NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"),
+        @NamedQuery(name="getGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:search,'%') order by u.name ASC"),
+        @NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parent is null and u.realm.id = :realm")
 })
 @Entity
 @Table(name="KEYCLOAK_GROUP")
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index 718d19a..683f7a3 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -20,29 +20,13 @@ package org.keycloak.models.jpa;
 import org.jboss.logging.Logger;
 import org.keycloak.connections.jpa.util.JpaUtils;
 import org.keycloak.migration.MigrationModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientTemplateModel;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RealmProvider;
-import org.keycloak.models.RoleContainerModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.jpa.entities.ClientEntity;
-import org.keycloak.models.jpa.entities.ClientTemplateEntity;
-import org.keycloak.models.jpa.entities.GroupEntity;
-import org.keycloak.models.jpa.entities.RealmEntity;
-import org.keycloak.models.jpa.entities.RoleEntity;
+import org.keycloak.models.*;
+import org.keycloak.models.jpa.entities.*;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -350,6 +334,24 @@ public class JpaRealmProvider implements RealmProvider {
     }
 
     @Override
+    public List<GroupModel> getTopLevelGroups(RealmModel realm, Integer first, Integer max) {
+        List<String> groupIds =  em.createNamedQuery("getTopLevelGroupIds", String.class)
+                .setParameter("realm", realm.getId())
+                .setFirstResult(first)
+                    .setMaxResults(max)
+                    .getResultList();
+        List<GroupModel> list = new ArrayList<>();
+        if(Objects.nonNull(groupIds) && !groupIds.isEmpty()) {
+            for (String id : groupIds) {
+                GroupModel group = getGroupById(id, realm);
+                list.add(group);
+            }
+        }
+
+        return Collections.unmodifiableList(list);
+    }
+
+    @Override
     public boolean removeGroup(RealmModel realm, GroupModel group) {
         if (group == null) {
             return false;
@@ -519,4 +521,21 @@ public class JpaRealmProvider implements RealmProvider {
         ClientTemplateAdapter adapter = new ClientTemplateAdapter(realm, em, session, app);
         return adapter;
     }
+
+    @Override
+    public List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
+        TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContaining", String.class)
+                .setParameter("realm", realm.getId())
+                .setParameter("search", search);
+        if(Objects.nonNull(first) && Objects.nonNull(max)) {
+            query= query.setFirstResult(first).setMaxResults(max);
+        }
+        List<String> groups =  query.getResultList();
+        if (Objects.isNull(groups)) return Collections.EMPTY_LIST;
+        List<GroupModel> list = new LinkedList<>();
+        for (String id : groups) {
+            list.add(session.realms().getGroupById(id, realm));
+        }
+        return Collections.unmodifiableList(list);
+    }
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 33ca943..e53ef2f 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -22,53 +22,13 @@ import org.keycloak.common.enums.SslRequired;
 import org.keycloak.common.util.MultivaluedHashMap;
 import org.keycloak.component.ComponentFactory;
 import org.keycloak.component.ComponentModel;
-import org.keycloak.models.AuthenticationExecutionModel;
-import org.keycloak.models.AuthenticationFlowModel;
-import org.keycloak.models.AuthenticatorConfigModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientTemplateModel;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.IdentityProviderMapperModel;
-import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelException;
-import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.PasswordPolicy;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RequiredActionProviderModel;
-import org.keycloak.models.RequiredCredentialModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity;
-import org.keycloak.models.jpa.entities.AuthenticationFlowEntity;
-import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity;
-import org.keycloak.models.jpa.entities.ClientEntity;
-import org.keycloak.models.jpa.entities.ClientTemplateEntity;
-import org.keycloak.models.jpa.entities.ComponentConfigEntity;
-import org.keycloak.models.jpa.entities.ComponentEntity;
-import org.keycloak.models.jpa.entities.GroupEntity;
-import org.keycloak.models.jpa.entities.IdentityProviderEntity;
-import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity;
-import org.keycloak.models.jpa.entities.RealmAttributeEntity;
-import org.keycloak.models.jpa.entities.RealmAttributes;
-import org.keycloak.models.jpa.entities.RealmEntity;
-import org.keycloak.models.jpa.entities.RequiredActionProviderEntity;
-import org.keycloak.models.jpa.entities.RequiredCredentialEntity;
-import org.keycloak.models.jpa.entities.RoleEntity;
+import org.keycloak.models.*;
+import org.keycloak.models.jpa.entities.*;
 import org.keycloak.models.utils.ComponentUtil;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
 import javax.persistence.EntityManager;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -360,7 +320,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         realm.setVerifyEmail(verifyEmail);
         em.flush();
     }
-    
+
     @Override
     public boolean isLoginWithEmailAllowed() {
         return realm.isLoginWithEmailAllowed();
@@ -372,7 +332,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         if (loginWithEmailAllowed) realm.setDuplicateEmailsAllowed(false);
         em.flush();
     }
-    
+
     @Override
     public boolean isDuplicateEmailsAllowed() {
         return realm.isDuplicateEmailsAllowed();
@@ -1720,6 +1680,16 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
     }
 
     @Override
+    public List<GroupModel> getTopLevelGroups(Integer first, Integer max) {
+        return session.realms().getTopLevelGroups(this, first, max);
+    }
+
+    @Override
+    public List<GroupModel> searchForGroupByName(String search, Integer first, Integer max) {
+        return session.realms().searchForGroupByName(this, search, first, max);
+    }
+
+    @Override
     public boolean removeGroup(GroupModel group) {
         return session.realms().removeGroup(this, group);
     }
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index f6484d6..f90a9d7 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -23,11 +23,7 @@ import org.keycloak.provider.ProviderEvent;
 import org.keycloak.storage.UserStorageProvider;
 import org.keycloak.storage.UserStorageProviderModel;
 
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -147,11 +143,11 @@ public interface RealmModel extends RoleContainerModel {
     boolean isVerifyEmail();
 
     void setVerifyEmail(boolean verifyEmail);
-    
+
     boolean isLoginWithEmailAllowed();
 
     void setLoginWithEmailAllowed(boolean loginWithEmailAllowed);
-    
+
     boolean isDuplicateEmailsAllowed();
 
     void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed);
@@ -402,6 +398,8 @@ public interface RealmModel extends RoleContainerModel {
     GroupModel getGroupById(String id);
     List<GroupModel> getGroups();
     List<GroupModel> getTopLevelGroups();
+    List<GroupModel> getTopLevelGroups(Integer first, Integer max);
+    List<GroupModel> searchForGroupByName(String search, Integer first, Integer max);
     boolean removeGroup(GroupModel group);
     void moveGroup(GroupModel group, GroupModel toParent);
 
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
index 4e6070f..3e1a1c9 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
@@ -42,6 +42,10 @@ public interface RealmProvider extends Provider {
 
     List<GroupModel> getTopLevelGroups(RealmModel realm);
 
+    List<GroupModel> getTopLevelGroups(RealmModel realm, Integer first, Integer max);
+
+    List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max);
+
     boolean removeGroup(RealmModel realm, GroupModel group);
 
     GroupModel createGroup(RealmModel realm, String name);
@@ -85,8 +89,6 @@ public interface RealmProvider extends Provider {
     ClientTemplateModel getClientTemplateById(String id, RealmModel realm);
     GroupModel getGroupById(String id, RealmModel realm);
 
-
-
     List<RealmModel> getRealms();
     boolean removeRealm(String id);
     void close();
diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java
index 16a965f..09111cc 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CachedRealmModel.java
@@ -18,7 +18,6 @@ package org.keycloak.models.cache;
 
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.provider.ProviderEvent;
 
 import java.util.concurrent.ConcurrentHashMap;
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 7431ed2..ab32a22 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -17,17 +17,6 @@
 
 package org.keycloak.models.utils;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
 import org.keycloak.authorization.AuthorizationProvider;
 import org.keycloak.authorization.model.Policy;
 import org.keycloak.authorization.model.Resource;
@@ -42,60 +31,15 @@ import org.keycloak.credential.CredentialModel;
 import org.keycloak.events.Event;
 import org.keycloak.events.admin.AdminEvent;
 import org.keycloak.events.admin.AuthDetails;
-import org.keycloak.models.AuthenticationExecutionModel;
-import org.keycloak.models.AuthenticationFlowModel;
-import org.keycloak.models.AuthenticatorConfigModel;
-import org.keycloak.models.AuthenticatedClientSessionModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientTemplateModel;
-import org.keycloak.models.FederatedIdentityModel;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.IdentityProviderMapperModel;
-import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelException;
-import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RequiredActionProviderModel;
-import org.keycloak.models.RequiredCredentialModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserConsentModel;
-import org.keycloak.models.UserCredentialModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.*;
 import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.representations.idm.AdminEventRepresentation;
-import org.keycloak.representations.idm.AuthDetailsRepresentation;
-import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
-import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
-import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
-import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.ClientTemplateRepresentation;
-import org.keycloak.representations.idm.ComponentRepresentation;
-import org.keycloak.representations.idm.ConfigPropertyRepresentation;
-import org.keycloak.representations.idm.CredentialRepresentation;
-import org.keycloak.representations.idm.EventRepresentation;
-import org.keycloak.representations.idm.FederatedIdentityRepresentation;
-import org.keycloak.representations.idm.GroupRepresentation;
-import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
-import org.keycloak.representations.idm.IdentityProviderRepresentation;
-import org.keycloak.representations.idm.ProtocolMapperRepresentation;
-import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
-import org.keycloak.representations.idm.RoleRepresentation;
-import org.keycloak.representations.idm.UserConsentRepresentation;
-import org.keycloak.representations.idm.UserRepresentation;
-import org.keycloak.representations.idm.UserSessionRepresentation;
-import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
-import org.keycloak.representations.idm.authorization.PolicyRepresentation;
-import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
-import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
-import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.representations.idm.*;
+import org.keycloak.representations.idm.authorization.*;
 import org.keycloak.storage.StorageId;
 
+import java.util.*;
+import java.util.stream.Collectors;
+
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -147,10 +91,32 @@ public class ModelToRepresentation {
         return rep;
     }
 
+    public static List<GroupRepresentation> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
+        List<GroupRepresentation> result = new LinkedList<>();
+        List<GroupModel> groups = realm.searchForGroupByName(search, first, max);
+        if (Objects.isNull(groups)) return result;
+        for (GroupModel group : groups) {
+            GroupRepresentation rep = toGroupHierarchy(group, false);
+            result.add(rep);
+        }
+        return result;
+    }
+
+    public static List<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) {
+        List<GroupRepresentation> hierarchy = new LinkedList<>();
+        List<GroupModel> groups = realm.getTopLevelGroups(first, max);
+        if (Objects.isNull(groups)) return hierarchy;
+        for (GroupModel group : groups) {
+            GroupRepresentation rep = toGroupHierarchy(group, full);
+            hierarchy.add(rep);
+        }
+        return hierarchy;
+    }
+
     public static List<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full) {
         List<GroupRepresentation> hierarchy = new LinkedList<>();
         List<GroupModel> groups = realm.getTopLevelGroups();
-        if (groups == null) return hierarchy;
+        if (Objects.isNull(groups)) return hierarchy;
         for (GroupModel group : groups) {
             GroupRepresentation rep = toGroupHierarchy(group, full);
             hierarchy.add(rep);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
index 5be1c0d..7c61fe9 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
@@ -26,20 +26,16 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.services.ErrorResponse;
 
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
+import javax.ws.rs.*;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.net.URI;
 import java.util.List;
-import org.keycloak.services.ErrorResponse;
+import java.util.Objects;
 
 /**
  * @resource Groups
@@ -71,10 +67,22 @@ public class GroupsResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<GroupRepresentation> getGroups() {
+    public List<GroupRepresentation> getGroupsByName(@QueryParam("search") String search,
+                                               @QueryParam("first") Integer firstResult,
+                                               @QueryParam("max") Integer maxResults) {
         auth.requireView();
 
-        return ModelToRepresentation.toGroupHierarchy(realm, false);
+        List<GroupRepresentation> results;
+
+        if (Objects.nonNull(search)) {
+            results = ModelToRepresentation.searchForGroupByName(realm, search.trim(), firstResult, maxResults);
+        } else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) {
+            results = ModelToRepresentation.toGroupHierarchy(realm, false, firstResult, maxResults);
+        } else {
+            results = ModelToRepresentation.toGroupHierarchy(realm, false);
+        }
+
+        return results;
     }
 
     /**
@@ -109,7 +117,7 @@ public class GroupsResource {
                 return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists.");
             }
         }
-        
+
         GroupModel child = null;
         Response.ResponseBuilder builder = Response.status(204);
         if (rep.getId() != null) {