keycloak-uncached

Changes

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..6c92204 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,27 +20,90 @@ 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;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @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);
+
+    /**
+     * Counts all groups.
+     * @return A map containing key "count" with number of groups as value.
+     */
+    @GET
+    @NoCache
+    @Path("count")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    Map<String, Long> count();
+
+    /**
+     * Counts groups by name search.
+     * @param search max number of occurrences
+     * @return A map containing key "count" with number of groups as value which matching with search.
+     */
+    @GET
+    @NoCache
+    @Path("count")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    Map<String, Long> count(@QueryParam("search") String search);
+
+    /**
+     * Counts groups by name search.
+     * @param onlyTopGroups <code>true</code> or <code>false</code> for filter only top level groups count
+     * @return A map containing key "count" with number of top level groups.
+     */
+    @GET
+    @NoCache
+    @Path("count")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    Map<String, Long> count(@QueryParam("top") @DefaultValue("true") boolean onlyTopGroups);
+
+    /**
      * 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 9925a69..7144c3b 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());
@@ -1233,11 +1213,31 @@ public class RealmAdapter implements CachedRealmModel {
     }
 
     @Override
+    public Long getGroupsCount(Boolean onlyTopGroups) {
+        return cacheSession.getGroupsCount(this, onlyTopGroups);
+    }
+
+    @Override
+    public Long getGroupsCountByNameContaining(String search) {
+        return cacheSession.getGroupsCountByNameContaining(this, search);
+    }
+
+    @Override
     public List<GroupModel> getTopLevelGroups() {
         return cacheSession.getTopLevelGroups(this);
     }
 
     @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 fdd5cce..318bb53 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
@@ -22,48 +22,14 @@ import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.models.ClientInitialAccessModel;
 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.*;
 
 
 /**
@@ -128,7 +94,6 @@ public class RealmCacheSession implements CacheRealmProvider {
     protected static final Logger logger = Logger.getLogger(RealmCacheSession.class);
     public static final String REALM_CLIENTS_QUERY_SUFFIX = ".realm.clients";
     public static final String ROLES_QUERY_SUFFIX = ".roles";
-    public static final String ROLE_BY_NAME_QUERY_SUFFIX = ".role.by-name";
     protected RealmCacheManager cache;
     protected KeycloakSession session;
     protected RealmProvider delegate;
@@ -873,10 +838,23 @@ public class RealmCacheSession implements CacheRealmProvider {
             }
             list.add(group);
         }
+
+        list.sort(Comparator.comparing(GroupModel::getName));
+
         return list;
     }
 
     @Override
+    public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
+        return getDelegate().getGroupsCount(realm, onlyTopGroups);
+    }
+
+    @Override
+    public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
+        return getDelegate().getGroupsCountByNameContaining(realm, search);
+    }
+
+    @Override
     public List<GroupModel> getTopLevelGroups(RealmModel realm) {
         String cacheKey = getTopGroupsQueryCacheKey(realm.getId());
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId());
@@ -909,10 +887,57 @@ public class RealmCacheSession implements CacheRealmProvider {
             }
             list.add(group);
         }
+
+        list.sort(Comparator.comparing(GroupModel::getName));
+
         return list;
     }
 
     @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);
+        }
+
+        list.sort(Comparator.comparing(GroupModel::getName));
+
+        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());
@@ -963,11 +988,9 @@ public class RealmCacheSession implements CacheRealmProvider {
         String groupId = eventToAdd.getId();
 
         // Check if we have existing event with bigger priority
-        boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> {
-
-            return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent);
-
-        }).findFirst().isPresent();
+        boolean eventAlreadyExists = invalidationEvents.stream()
+                .anyMatch((InvalidationEvent event) -> (event.getId().equals(groupId)) &&
+                        (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent));
 
         if (!eventAlreadyExists) {
             invalidationEvents.add(eventToAdd);
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..e01ad0c 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,10 @@ 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"),
+        @NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm.id = :realm"),
+        @NamedQuery(name="getTopLevelGroupCount", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.parent is null")
 })
 @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 3a85825..3592a56 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
@@ -17,6 +17,8 @@
 
 package org.keycloak.models.jpa;
 
+import com.sun.org.apache.xpath.internal.operations.Bool;
+import org.apache.commons.codec.binary.StringUtils;
 import org.jboss.logging.Logger;
 import org.keycloak.common.util.Time;
 import org.keycloak.connections.jpa.util.JpaUtils;
@@ -40,12 +42,7 @@ 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;
 
 /**
@@ -343,22 +340,62 @@ public class JpaRealmProvider implements RealmProvider {
 
         return ref.getGroups().stream()
                 .map(g -> session.realms().getGroupById(g.getId(), realm))
+                .sorted(Comparator.comparing(GroupModel::getName))
                 .collect(Collectors.collectingAndThen(
                         Collectors.toList(), Collections::unmodifiableList));
     }
 
     @Override
+    public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
+        String query = "getGroupCount";
+        if(Objects.equals(onlyTopGroups, Boolean.TRUE)) {
+            query = "getTopLevelGroupCount";
+        }
+        Long count = em.createNamedQuery(query, Long.class)
+                .setParameter("realm", realm.getId())
+                .getSingleResult();
+
+        return count;
+    }
+
+    @Override
+    public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
+        return (long) searchForGroupByName(realm, search, null, null).size();
+    }
+
+    @Override
     public List<GroupModel> getTopLevelGroups(RealmModel realm) {
         RealmEntity ref = em.getReference(RealmEntity.class, realm.getId());
 
         return ref.getGroups().stream()
                 .filter(g -> g.getParent() == null)
                 .map(g -> session.realms().getGroupById(g.getId(), realm))
+                .sorted(Comparator.comparing(GroupModel::getName))
                 .collect(Collectors.collectingAndThen(
                         Collectors.toList(), Collections::unmodifiableList));
     }
 
     @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);
+            }
+        }
+
+        list.sort(Comparator.comparing(GroupModel::getName));
+
+        return Collections.unmodifiableList(list);
+    }
+
+    @Override
     public boolean removeGroup(RealmModel realm, GroupModel group) {
         if (group == null) {
             return false;
@@ -548,6 +585,31 @@ public class JpaRealmProvider implements RealmProvider {
     }
 
     @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 ArrayList<>();
+        for (String id : groups) {
+            GroupModel groupById = session.realms().getGroupById(id, realm);
+            while(Objects.nonNull(groupById.getParentId())) {
+                groupById = session.realms().getGroupById(groupById.getParentId(), realm);
+            }
+            if(!list.contains(groupById)) {
+                list.add(groupById);
+            }
+        }
+        list.sort(Comparator.comparing(GroupModel::getName));
+
+        return Collections.unmodifiableList(list);
+    }
+
+    @Override
     public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
         RealmEntity realmEntity = em.find(RealmEntity.class, realm.getId());
 
@@ -595,7 +657,7 @@ public class JpaRealmProvider implements RealmProvider {
         List<ClientInitialAccessEntity> entities = query.getResultList();
 
         return entities.stream()
-                .map(entity -> entityToModel(entity))
+                .map(this::entityToModel)
                 .collect(Collectors.toList());
     }
 
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 cd814f4..3b07a73 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();
@@ -1727,11 +1687,31 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
     }
 
     @Override
+    public Long getGroupsCount(Boolean onlyTopGroups) {
+        return session.realms().getGroupsCount(this, onlyTopGroups);
+    }
+
+    @Override
+    public Long getGroupsCountByNameContaining(String search) {
+        return session.realms().getGroupsCountByNameContaining(this, search);
+    }
+
+    @Override
     public List<GroupModel> getTopLevelGroups() {
         return session.realms().getTopLevelGroups(this);
     }
 
     @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 ff1cfd7..e88c59f 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);
@@ -404,7 +400,11 @@ public interface RealmModel extends RoleContainerModel {
 
     GroupModel getGroupById(String id);
     List<GroupModel> getGroups();
+    Long getGroupsCount(Boolean onlyTopGroups);
+    Long getGroupsCountByNameContaining(String search);
     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 f3a26f1..d14f2d6 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
@@ -40,8 +40,16 @@ public interface RealmProvider extends Provider {
 
     List<GroupModel> getGroups(RealmModel realm);
 
+    Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups);
+
+    Long getGroupsCountByNameContaining(RealmModel realm, String search);
+
     List<GroupModel> getTopLevelGroups(RealmModel realm);
 
+    List<GroupModel> getTopLevelGroups(RealmModel realm, Integer first, Integer max);
+
+    List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max);
+
     boolean removeGroup(RealmModel realm, GroupModel group);
 
     GroupModel createGroup(RealmModel realm, String name);
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 172147a..82089a1 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.AuthenticatedClientSessionModel;
-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.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 $
@@ -131,12 +75,7 @@ public class ModelToRepresentation {
             } else {
                 ClientModel client = (ClientModel)role.getContainer();
                 String clientId = client.getClientId();
-                List<String> currentClientRoles = clientRoleNames.get(clientId);
-                if (currentClientRoles == null) {
-                    currentClientRoles = new ArrayList<>();
-                    clientRoleNames.put(clientId, currentClientRoles);
-                }
-
+                List<String> currentClientRoles = clientRoleNames.computeIfAbsent(clientId, k -> new ArrayList<>());
                 currentClientRoles.add(role.getName());
             }
         }
@@ -147,10 +86,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);
@@ -188,9 +149,7 @@ public class ModelToRepresentation {
 
         List<String> reqActions = new ArrayList<String>();
         Set<String> requiredActions = user.getRequiredActions();
-        for (String ra : requiredActions){
-            reqActions.add(ra);
-        }
+        reqActions.addAll(requiredActions);
 
         rep.setRequiredActions(reqActions);
 
@@ -644,11 +603,7 @@ public class ModelToRepresentation {
         Map<String, List<String>> grantedProtocolMappers = new HashMap<String, List<String>>();
         for (ProtocolMapperModel protocolMapper : model.getGrantedProtocolMappers()) {
             String protocol = protocolMapper.getProtocol();
-            List<String> currentProtocolMappers = grantedProtocolMappers.get(protocol);
-            if (currentProtocolMappers == null) {
-                currentProtocolMappers = new LinkedList<String>();
-                grantedProtocolMappers.put(protocol, currentProtocolMappers);
-            }
+            List<String> currentProtocolMappers = grantedProtocolMappers.computeIfAbsent(protocol, k -> new LinkedList<String>());
             currentProtocolMappers.add(protocolMapper.getName());
         }
 
@@ -661,11 +616,7 @@ public class ModelToRepresentation {
                 ClientModel client2 = (ClientModel) role.getContainer();
 
                 String clientId2 = client2.getClientId();
-                List<String> currentClientRoles = grantedClientRoles.get(clientId2);
-                if (currentClientRoles == null) {
-                    currentClientRoles = new LinkedList<String>();
-                    grantedClientRoles.put(clientId2, currentClientRoles);
-                }
+                List<String> currentClientRoles = grantedClientRoles.computeIfAbsent(clientId2, k -> new LinkedList<String>());
                 currentClientRoles.add(role.getName());
             }
         }
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 2a1909b..15be7ae 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
@@ -16,6 +16,7 @@
  */
 package org.keycloak.services.resources.admin;
 
+import org.apache.http.HttpStatus;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.NotFoundException;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
@@ -26,21 +27,22 @@ 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.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import org.keycloak.services.ErrorResponse;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
+import twitter4j.JSONException;
+import twitter4j.JSONObject;
 
 /**
  * @resource Groups
@@ -71,10 +73,22 @@ public class GroupsResource {
     @GET
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<GroupRepresentation> getGroups() {
+    public List<GroupRepresentation> getGroups(@QueryParam("search") String search,
+                                               @QueryParam("first") Integer firstResult,
+                                               @QueryParam("max") Integer maxResults) {
         auth.groups().requireList();
 
-        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;
     }
 
     /**
@@ -95,6 +109,28 @@ public class GroupsResource {
     }
 
     /**
+     * Returns the groups counts.
+     *
+     * @return
+     */
+    @GET
+    @NoCache
+    @Path("count")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, Long> getGroupCount(@QueryParam("search") String search,
+                                           @QueryParam("top") @DefaultValue("false") boolean onlyTopGroups) {
+        Long results;
+        Map<String, Long> map = new HashMap<>();
+        if (Objects.nonNull(search)) {
+            results = realm.getGroupsCountByNameContaining(search);
+        } else {
+            results = realm.getGroupsCount(onlyTopGroups);
+        }
+        map.put("count", results);
+        return map;
+    }
+
+    /**
      * 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.
      *
@@ -105,13 +141,12 @@ public class GroupsResource {
     public Response addTopLevelGroup(GroupRepresentation rep) {
         auth.groups().requireManage();
 
-        for (GroupModel group : realm.getGroups()) {
-            if (group.getName().equals(rep.getName())) {
-                return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists.");
-            }
+        List<GroupRepresentation> search = ModelToRepresentation.searchForGroupByName(realm, rep.getName(), 0, 1);
+        if (search != null && !search.isEmpty() && Objects.equals(search.get(0).getName(), rep.getName())) {
+            return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists.");
         }
-        
-        GroupModel child = null;
+
+        GroupModel child;
         Response.ResponseBuilder builder = Response.status(204);
         if (rep.getId() != null) {
             child = realm.getGroupById(rep.getId());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
index b7274ec..fd58f75 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
@@ -92,7 +92,6 @@ public class GroupTest extends AbstractGroupTest {
         user.setCredentials(credentials);
         users.add(user);
 
-
         List<ClientRepresentation> clients = testRealmRep.getClients();
 
         ClientRepresentation client = new ClientRepresentation();
@@ -155,16 +154,16 @@ public class GroupTest extends AbstractGroupTest {
     @Test
     public void doNotAllowSameGroupNameAtSameLevel() throws Exception {
         RealmResource realm = adminClient.realms().realm("test");
-        
+
         GroupRepresentation topGroup = new GroupRepresentation();
         topGroup.setName("top");
         topGroup = createGroup(realm, topGroup);
-        
+
         GroupRepresentation anotherTopGroup = new GroupRepresentation();
         anotherTopGroup.setName("top");
         Response response = realm.groups().add(anotherTopGroup);
         assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed
-        
+
         GroupRepresentation level2Group = new GroupRepresentation();
         level2Group.setName("level2");
         response = realm.groups().group(topGroup.getId()).subGroup(level2Group);
@@ -177,7 +176,7 @@ public class GroupTest extends AbstractGroupTest {
         response.close();
         assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed
     }
-    
+
     @Test
     public void createAndTestGroups() throws Exception {
         RealmResource realm = adminClient.realms().realm("test");
@@ -206,7 +205,7 @@ public class GroupTest extends AbstractGroupTest {
         GroupRepresentation topGroup = new GroupRepresentation();
         topGroup.setName("top");
         topGroup = createGroup(realm, topGroup);
-        
+
         List<RoleRepresentation> roles = new LinkedList<>();
         roles.add(topRole);
         realm.groups().group(topGroup.getId()).roles().realmLevel().add(roles);
@@ -634,4 +633,59 @@ public class GroupTest extends AbstractGroupTest {
         assertEquals(110, group.members(0, 1000).size());
         assertEquals(110, group.members(-1, -2).size());
     }
+
+    @Test
+    public void searchAndCountGroups() throws Exception {
+        String firstGroupId = "";
+
+        RealmResource realm = adminClient.realms().realm("test");
+
+        // Clean up all test groups
+        for (GroupRepresentation group : realm.groups().groups()) {
+            GroupResource resource = realm.groups().group(group.getId());
+            resource.remove();
+            assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.groupPath(group.getId()), ResourceType.GROUP);
+        }
+
+        // Add 20 new groups with known names
+        for (int i=0;i<20;i++) {
+            GroupRepresentation group = new GroupRepresentation();
+            group.setName("group"+i);
+            group = createGroup(realm, group);
+            if(i== 0) {
+                firstGroupId = group.getId();
+            }
+        }
+
+        // Get groups by search and pagination
+        List<GroupRepresentation> allGroups = realm.groups().groups();
+        assertEquals(20, allGroups.size());
+
+        List<GroupRepresentation> slice = realm.groups().groups(5, 7);
+        assertEquals(7, slice.size());
+
+        List<GroupRepresentation> search = realm.groups().groups("group1",0,20);
+        assertEquals(11, search.size());
+        for(GroupRepresentation group : search) {
+            assertTrue(group.getName().contains("group1"));
+        }
+
+        List<GroupRepresentation> noResultSearch = realm.groups().groups("abcd",0,20);
+        assertEquals(0, noResultSearch.size());
+
+        // Count
+        assertEquals(new Long(allGroups.size()), realm.groups().count().get("count"));
+        assertEquals(new Long(search.size()), realm.groups().count("group1").get("count"));
+        assertEquals(new Long(noResultSearch.size()), realm.groups().count("abcd").get("count"));
+
+        // Add a subgroup for onlyTopLevel flag testing
+        GroupRepresentation level2Group = new GroupRepresentation();
+        level2Group.setName("group1111");
+        Response response = realm.groups().group(firstGroupId).subGroup(level2Group);
+        response.close();
+        assertAdminEvents.assertEvent("test", OperationType.CREATE, AdminEventPaths.groupSubgroupsPath(firstGroupId), level2Group, ResourceType.GROUP);
+
+        assertEquals(new Long(allGroups.size()), realm.groups().count(true).get("count"));
+        assertEquals(new Long(allGroups.size() + 1), realm.groups().count(false).get("count"));
+    }
 }
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 1620996..6cbba48 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1036,6 +1036,7 @@ group-membership.tooltip=Groups user is a member of. Select a listed group and c
 membership.available-groups.tooltip=Groups a user can join. Select a group and click the join button.
 table-of-realm-users=Table of Realm Users
 view-all-users=View all users
+view-all-groups=View all groups
 unlock-users=Unlock users
 no-users-available=No users available
 users.instruction=Please enter a search, or click on view all users
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index 4e6de53..01ce2a5 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -802,6 +802,9 @@ module.config([ '$routeProvider', function($routeProvider) {
                 },
                 groups : function(GroupListLoader) {
                     return GroupListLoader();
+                },
+                groupsCount : function(GroupCountLoader) {
+                    return GroupCountLoader();
                 }
             },
             controller : 'GroupListCtrl'
@@ -1936,7 +1939,7 @@ module.factory('spinnerInterceptor', function($q, $window, $rootScope, $location
                 $('#loading').hide();
             }
             return response;
-        }, 
+        },
         responseError: function(response) {
             resourceRequests--;
             if (resourceRequests == 0) {
@@ -1956,7 +1959,7 @@ module.factory('errorInterceptor', function($q, $window, $rootScope, $location, 
     return {
         response: function(response) {
             return response;
-        }, 
+        },
         responseError: function(response) {
             if (response.status == 401) {
                 Auth.authz.logout();
@@ -2198,17 +2201,17 @@ module.directive('kcEnter', function() {
 
 module.directive('kcSave', function ($compile, $timeout, Notifications) {
     var clickDelay = 500; // 500 ms
-    
+
     return {
         restrict: 'A',
         link: function ($scope, elem, attr, ctrl) {
             elem.addClass("btn btn-primary");
             elem.attr("type","submit");
-            
+
             var disabled = false;
             elem.on('click', function(evt) {
                 if ($scope.hasOwnProperty("changed") && !$scope.changed) return;
-                
+
                 // KEYCLOAK-4121: Prevent double form submission
                 if (disabled) {
                     evt.preventDefault();
@@ -2218,7 +2221,7 @@ module.directive('kcSave', function ($compile, $timeout, Notifications) {
                     disabled = true;
                     $timeout(function () { disabled = false; }, clickDelay, false);
                 }
-                
+
                 $scope.$apply(function() {
                     var form = elem.closest('form');
                     if (form && form.attr('name')) {
@@ -2881,35 +2884,35 @@ module.directive('kcOnReadFile', function ($parse) {
 
 module.controller('PagingCtrl', function ($scope) {
     $scope.currentPageInput = 1;
-    
+
     $scope.firstPage = function() {
         if (!$scope.hasPrevious()) return;
         $scope.currentPage = 1;
         $scope.currentPageInput = 1;
     };
-    
+
     $scope.lastPage = function() {
         if (!$scope.hasNext()) return;
         $scope.currentPage = $scope.numberOfPages;
         $scope.currentPageInput = $scope.numberOfPages;
     };
-    
+
     $scope.previousPage = function() {
         if (!$scope.hasPrevious()) return;
         $scope.currentPage--;
         $scope.currentPageInput = $scope.currentPage;
     };
-    
+
     $scope.nextPage = function() {
         if (!$scope.hasNext()) return;
         $scope.currentPage++;
         $scope.currentPageInput = $scope.currentPage;
     };
-    
+
     $scope.hasNext = function() {
         return $scope.currentPage < $scope.numberOfPages;
     };
-    
+
     $scope.hasPrevious = function() {
         return $scope.currentPage > 1;
     };
@@ -2939,11 +2942,11 @@ module.directive('kcValidPage', function() {
                if (viewValue >= 1 && viewValue <= scope.numberOfPages) {
                    scope.currentPage = viewValue;
                }
-               
+
                return true;
            }
        }
-   } 
+   }
 });
 
 // filter used for paged tables
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
index aa0cfad..bc3691f 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
@@ -1,31 +1,102 @@
-module.controller('GroupListCtrl', function($scope, $route, realm, groups, Groups, Group, GroupChildren, Notifications, $location, Dialog) {
+module.controller('GroupListCtrl', function($scope, $route, $q, realm, groups, groupsCount, Groups, GroupsCount, Group, GroupChildren, Notifications, $location, Dialog) {
     $scope.realm = realm;
     $scope.groupList = [
-        {"id" : "realm", "name": "Groups",
-            "subGroups" : groups}
+        {
+            "id" : "realm",
+            "name": "Groups",
+            "subGroups" : groups
+        }
     ];
 
+    $scope.searchTerms = '';
+    $scope.currentPage = 1;
+    $scope.currentPageInput = $scope.currentPage;
+    $scope.pageSize = groups.length;
+    $scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize);
+
     $scope.tree = [];
 
+    var refreshGroups = function (search) {
+        var queryParams = {
+            realm : realm.id,
+            first : ($scope.currentPage * $scope.pageSize) - $scope.pageSize,
+            max : $scope.pageSize
+        };
+        var countParams = {
+            realm : realm.id,
+            top : 'true'
+        };
+
+        if(angular.isDefined(search) && search !== '') {
+            queryParams.search = search;
+            countParams.search = search;
+        }
+
+        var promiseGetGroups = $q.defer();
+        Groups.query(queryParams, function(entry) {
+            promiseGetGroups.resolve(entry);
+        }, function() {
+            promiseGetGroups.reject('Unable to fetch ' + queryParams);
+        });
+        var promiseGetGroupsChain   = promiseGetGroups.promise.then(function(entry) {
+            groups = entry;
+            $scope.groupList = [
+                {
+                    "id" : "realm",
+                    "name": "Groups",
+                    "subGroups" : groups
+                }
+            ];
+        });
+
+        var promiseCount = $q.defer();
+        GroupsCount.query(countParams, function(entry) {
+            promiseCount.resolve(entry);
+        }, function() {
+            promiseCount.reject('Unable to fetch ' + countParams);
+        });
+        var promiseCountChain   = promiseCount.promise.then(function(entry) {
+            groupsCount = entry;
+            $scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize);
+        });
+    };
+
+    $scope.$watch('currentPage', function(newValue, oldValue) {
+        if(newValue !== oldValue) {
+            refreshGroups($scope.searchTerms);
+        }
+    });
+
+    $scope.clearSearch = function() {
+        $scope.searchTerms = '';
+        $scope.currentPage = 1;
+        refreshGroups();
+    };
+
+    $scope.searchGroup = function() {
+        $scope.currentPage = 1;
+        refreshGroups($scope.searchTerms);
+    };
+
     $scope.edit = function(selected) {
-        if (selected.id == 'realm') return;
+        if (selected.id === 'realm') return;
         $location.url("/realms/" + realm.realm + "/groups/" + selected.id);
-    }
+    };
 
     $scope.cut = function(selected) {
         $scope.cutNode = selected;
-    }
+    };
 
     $scope.isDisabled = function() {
         if (!$scope.tree.currentNode) return true;
-        return $scope.tree.currentNode.id == 'realm';
-    }
+        return $scope.tree.currentNode.id === 'realm';
+    };
 
     $scope.paste = function(selected) {
-        if (selected == null) return;
-        if ($scope.cutNode == null) return;
-        if (selected.id == $scope.cutNode.id) return;
-        if (selected.id == 'realm') {
+        if (selected === null) return;
+        if ($scope.cutNode === null) return;
+        if (selected.id === $scope.cutNode.id) return;
+        if (selected.id === 'realm') {
             Groups.save({realm: realm.realm}, {id:$scope.cutNode.id}, function() {
                 $route.reload();
                 Notifications.success("Group moved.");
@@ -41,10 +112,10 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
 
         }
 
-    }
+    };
 
     $scope.remove = function(selected) {
-        if (selected == null) return;
+        if (selected === null) return;
         Dialog.confirmDelete(selected.name, 'group', function() {
             Group.remove({ realm: realm.realm, groupId : selected.id }, function() {
                 $route.reload();
@@ -52,7 +123,7 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
             });
         });
 
-    }
+    };
 
     $scope.createGroup = function(selected) {
         var parent = 'realm';
@@ -61,13 +132,13 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
         }
         $location.url("/create/group/" + realm.realm + '/parent/' + parent);
 
-    }
+    };
     var isLeaf = function(node) {
-        return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
-    }
+        return node.id !== "realm" && (!node.subGroups || node.subGroups.length === 0);
+    };
 
     $scope.getGroupClass = function(node) {
-        if (node.id == "realm") {
+        if (node.id === "realm") {
             return 'pficon pficon-users';
         }
         if (isLeaf(node)) {
@@ -77,12 +148,12 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
         if (node.subGroups.length && !node.collapsed) return 'expanded';
         return 'collapsed';
 
-    }
+    };
 
     $scope.getSelectedClass = function(node) {
         if (node.selected) {
             return 'selected';
-        } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+        } else if ($scope.cutNode && $scope.cutNode.id === node.id) {
             return 'cut';
         }
         return undefined;
@@ -95,8 +166,8 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
     $scope.group = {};
     $scope.save = function() {
         console.log('save!!!');
-        if (parentId == 'realm') {
-            console.log('realm')
+        if (parentId === 'realm') {
+            console.log('realm');
             Groups.save({realm: realm.realm}, $scope.group, function(data, headers) {
                 var l = headers().location;
 
@@ -120,7 +191,7 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
 
         }
 
-    }
+    };
     $scope.cancel = function() {
         $location.url("/realms/" + realm.realm + "/groups");
     };
@@ -176,8 +247,7 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou
         var attrs = $scope.group.attributes;
         for (var attribute in attrs) {
             if (typeof attrs[attribute] === "string") {
-                var attrVals = attrs[attribute].split("##");
-                attrs[attribute] = attrVals;
+                attrs[attribute] = attrs[attribute].split("##");
             }
         }
     }
@@ -186,8 +256,7 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou
         var attrs = group.attributes;
         for (var attribute in attrs) {
             if (typeof attrs[attribute] === "object") {
-                var attrVals = attrs[attribute].join("##");
-                attrs[attribute] = attrVals;
+                attrs[attribute] = attrs[attribute].join("##");
             }
         }
     }
@@ -212,8 +281,8 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou
 });
 
 module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, clients, client, Notifications, GroupRealmRoleMapping,
-                                                  GroupClientRoleMapping, GroupAvailableRealmRoleMapping, GroupAvailableClientRoleMapping,
-                                                  GroupCompositeRealmRoleMapping, GroupCompositeClientRoleMapping) {
+                                                   GroupClientRoleMapping, GroupAvailableRealmRoleMapping, GroupAvailableClientRoleMapping,
+                                                   GroupCompositeRealmRoleMapping, GroupCompositeClientRoleMapping) {
     $scope.realm = realm;
     $scope.group = group;
     $scope.selectedRealmRoles = [];
@@ -237,70 +306,70 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, 
         $scope.selectedRealmRoles = [];
         $http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm',
             roles).then(function() {
-                $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.selectedRealmMappings = [];
-                $scope.selectRealmRoles = [];
-                if ($scope.targetClient) {
-                    console.log('load available');
-                    $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                    $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                    $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                    $scope.selectedClientRoles = [];
-                    $scope.selectedClientMappings = [];
-                }
-                Notifications.success("Role mappings updated.");
+            $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.selectedRealmMappings = [];
+            $scope.selectRealmRoles = [];
+            if ($scope.targetClient) {
+                console.log('load available');
+                $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+                $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+                $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+                $scope.selectedClientRoles = [];
+                $scope.selectedClientMappings = [];
+            }
+            Notifications.success("Role mappings updated.");
 
-            });
+        });
     };
 
     $scope.deleteRealmRole = function() {
         $http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm',
             {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() {
-                $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.selectedRealmMappings = [];
-                $scope.selectRealmRoles = [];
-                if ($scope.targetClient) {
-                    console.log('load available');
-                    $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                    $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                    $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                    $scope.selectedClientRoles = [];
-                    $scope.selectedClientMappings = [];
-                }
-                Notifications.success("Role mappings updated.");
-            });
+            $scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.selectedRealmMappings = [];
+            $scope.selectRealmRoles = [];
+            if ($scope.targetClient) {
+                console.log('load available');
+                $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+                $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+                $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+                $scope.selectedClientRoles = [];
+                $scope.selectedClientMappings = [];
+            }
+            Notifications.success("Role mappings updated.");
+        });
     };
 
     $scope.addClientRole = function() {
         $http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id,
             $scope.selectedClientRoles).then(function() {
-                $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                $scope.selectedClientRoles = [];
-                $scope.selectedClientMappings = [];
-                $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                Notifications.success("Role mappings updated.");
-            });
+            $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+            $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+            $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+            $scope.selectedClientRoles = [];
+            $scope.selectedClientMappings = [];
+            $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            Notifications.success("Role mappings updated.");
+        });
     };
 
     $scope.deleteClientRole = function() {
         $http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id,
             {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() {
-                $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
-                $scope.selectedClientRoles = [];
-                $scope.selectedClientMappings = [];
-                $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
-                Notifications.success("Role mappings updated.");
-            });
+            $scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+            $scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+            $scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
+            $scope.selectedClientRoles = [];
+            $scope.selectedClientMappings = [];
+            $scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            $scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
+            Notifications.success("Role mappings updated.");
+        });
     };
 
 
@@ -332,13 +401,13 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
         groupId: group.id,
         max : 5,
         first : 0
-    }
+    };
 
 
     $scope.firstPage = function() {
         $scope.query.first = 0;
         $scope.searchQuery();
-    }
+    };
 
     $scope.previousPage = function() {
         $scope.query.first -= parseInt($scope.query.max);
@@ -346,12 +415,12 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
             $scope.query.first = 0;
         }
         $scope.searchQuery();
-    }
+    };
 
     $scope.nextPage = function() {
         $scope.query.first += parseInt($scope.query.max);
         $scope.searchQuery();
-    }
+    };
 
     $scope.searchQuery = function() {
         console.log("query.search: " + $scope.query.search);
@@ -368,7 +437,7 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
 
 });
 
-module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications, $location, Dialog) {
+module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications) {
     $scope.realm = realm;
     $scope.groupList = groups;
     $scope.selectedGroup = null;
@@ -383,7 +452,7 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
         if (!$scope.tree.currentNode) {
             Notifications.error('Please select a group to add');
             return;
-        };
+        }
 
         DefaultGroups.update({realm: realm.realm, groupId: $scope.tree.currentNode.id}, function() {
             Notifications.success('Added default group');
@@ -401,11 +470,11 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
     };
 
     var isLeaf = function(node) {
-        return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
+        return node.id !== "realm" && (!node.subGroups || node.subGroups.length === 0);
     };
 
     $scope.getGroupClass = function(node) {
-        if (node.id == "realm") {
+        if (node.id === "realm") {
             return 'pficon pficon-users';
         }
         if (isLeaf(node)) {
@@ -415,12 +484,12 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
         if (node.subGroups.length && !node.collapsed) return 'expanded';
         return 'collapsed';
 
-    }
+    };
 
     $scope.getSelectedClass = function(node) {
         if (node.selected) {
             return 'selected';
-        } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+        } else if ($scope.cutNode && $scope.cutNode.id === node.id) {
             return 'cut';
         }
         return undefined;
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index 044414c..fb49c64 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -15,7 +15,7 @@ module.factory('Loader', function($q) {
 			});
 			return delay.promise;
 		};
-	}
+	};
 	loader.query = function(service, id) {
 		return function() {
 			var i = id && id();
@@ -27,7 +27,7 @@ module.factory('Loader', function($q) {
 			});
 			return delay.promise;
 		};
-	}
+	};
 	return loader;
 });
 
@@ -490,7 +490,18 @@ module.factory('AuthenticationConfigLoader', function(Loader, AuthenticationConf
 module.factory('GroupListLoader', function(Loader, Groups, $route, $q) {
     return Loader.query(Groups, function() {
         return {
-            realm : $route.current.params.realm
+            realm : $route.current.params.realm,
+            first : 0,
+            max : 20
+        }
+    });
+});
+
+module.factory('GroupCountLoader', function(Loader, GroupsCount, $route, $q) {
+    return Loader.query(GroupsCount, function() {
+        return {
+            realm : $route.current.params.realm,
+            top : true
         }
     });
 });
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index a9935f4..b68af10 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -3,7 +3,7 @@
 var module = angular.module('keycloak.services', [ 'ngResource', 'ngRoute' ]);
 
 module.service('Dialog', function($modal) {
-	var dialog = {};
+    var dialog = {};
 
     var openDialog = function(title, message, btns, template) {
         var controller = function($scope, $modalInstance, title, message, btns) {
@@ -36,15 +36,15 @@ module.service('Dialog', function($modal) {
         }).result;
     }
 
-	var escapeHtml = function(str) {
-		var div = document.createElement('div');
-		div.appendChild(document.createTextNode(str));
-		return div.innerHTML;
-	};
+    var escapeHtml = function(str) {
+        var div = document.createElement('div');
+        div.appendChild(document.createTextNode(str));
+        return div.innerHTML;
+    };
 
-	dialog.confirmDelete = function(name, type, success) {
-		var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1));
-		var msg = 'Are you sure you want to permanently delete the ' + type + ' ' + name + '?';
+    dialog.confirmDelete = function(name, type, success) {
+        var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1));
+        var msg = 'Are you sure you want to permanently delete the ' + type + ' ' + name + '?';
         var btns = {
             ok: {
                 label: 'Delete',
@@ -57,7 +57,7 @@ module.service('Dialog', function($modal) {
         }
 
         openDialog(title, msg, btns, '/templates/kc-modal.html').then(success);
-	}
+    }
 
     dialog.confirmGenerateKeys = function(name, type, success) {
         var title = 'Generate new keys for realm';
@@ -138,10 +138,10 @@ module.service('CopyDialog', function($modal) {
 });
 
 module.factory('Notifications', function($rootScope, $timeout) {
-	// time (in ms) the notifications are shown
-	var delay = 5000;
+    // time (in ms) the notifications are shown
+    var delay = 5000;
 
-	var notifications = {};
+    var notifications = {};
     notifications.current = { display: false };
     notifications.current.remove = function() {
         if (notifications.scheduled) {
@@ -157,9 +157,9 @@ module.factory('Notifications', function($rootScope, $timeout) {
 
     $rootScope.notification = notifications.current;
 
-	notifications.message = function(type, header, message) {
+    notifications.message = function(type, header, message) {
         notifications.current.remove();
-        
+
         notifications.current.type = type;
         notifications.current.header = header;
         notifications.current.message = message;
@@ -170,25 +170,25 @@ module.factory('Notifications', function($rootScope, $timeout) {
         }, delay);
 
         console.debug("Added message");
-	}
+    }
 
-	notifications.info = function(message) {
-		notifications.message("info", "Info!", message);
-	};
+    notifications.info = function(message) {
+        notifications.message("info", "Info!", message);
+    };
 
-	notifications.success = function(message) {
-		notifications.message("success", "Success!", message);
-	};
+    notifications.success = function(message) {
+        notifications.message("success", "Success!", message);
+    };
 
-	notifications.error = function(message) {
-		notifications.message("danger", "Error!", message);
-	};
+    notifications.error = function(message) {
+        notifications.message("danger", "Error!", message);
+    };
 
-	notifications.warn = function(message) {
-		notifications.message("warning", "Warning!", message);
-	};
+    notifications.warn = function(message) {
+        notifications.message("warning", "Warning!", message);
+    };
 
-	return notifications;
+    return notifications;
 });
 
 
@@ -256,12 +256,12 @@ module.factory('ComponentUtils', function() {
 });
 
 module.factory('Realm', function($resource) {
-	return $resource(authUrl + '/admin/realms/:id', {
-		id : '@realm'
-	}, {
-		update : {
-			method : 'PUT'
-		},
+    return $resource(authUrl + '/admin/realms/:id', {
+        id : '@realm'
+    }, {
+        update : {
+            method : 'PUT'
+        },
         create : {
             method : 'POST',
             params : { id : ''}
@@ -344,9 +344,9 @@ module.factory('RealmSMTPConnectionTester', function($resource) {
         realm : '@realm',
         config : '@config'
     }, {
-       send: {
-           method: 'POST'
-       }
+        send: {
+            method: 'POST'
+        }
     });
 });
 
@@ -796,67 +796,67 @@ function roleControl($scope, realm, role, roles, clients,
     $scope.addRealmRole = function() {
         $scope.compositeSwitchDisabled=true;
         $http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
-                $scope.selectedRealmRoles).then(function() {
-                for (var i = 0; i < $scope.selectedRealmRoles.length; i++) {
-                    var role = $scope.selectedRealmRoles[i];
-                    var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]);
-                    if (idx != -1) {
-                        $scope.realmRoles.splice(idx, 1);
-                        $scope.realmMappings.push(role);
-                    }
+            $scope.selectedRealmRoles).then(function() {
+            for (var i = 0; i < $scope.selectedRealmRoles.length; i++) {
+                var role = $scope.selectedRealmRoles[i];
+                var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]);
+                if (idx != -1) {
+                    $scope.realmRoles.splice(idx, 1);
+                    $scope.realmMappings.push(role);
                 }
-                $scope.selectedRealmRoles = [];
-                Notifications.success("Role added to composite.");
-            });
+            }
+            $scope.selectedRealmRoles = [];
+            Notifications.success("Role added to composite.");
+        });
     };
 
     $scope.deleteRealmRole = function() {
         $scope.compositeSwitchDisabled=true;
         $http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
             {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).then(function() {
-                for (var i = 0; i < $scope.selectedRealmMappings.length; i++) {
-                    var role = $scope.selectedRealmMappings[i];
-                    var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]);
-                    if (idx != -1) {
-                        $scope.realmMappings.splice(idx, 1);
-                        $scope.realmRoles.push(role);
-                    }
+            for (var i = 0; i < $scope.selectedRealmMappings.length; i++) {
+                var role = $scope.selectedRealmMappings[i];
+                var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]);
+                if (idx != -1) {
+                    $scope.realmMappings.splice(idx, 1);
+                    $scope.realmRoles.push(role);
                 }
-                $scope.selectedRealmMappings = [];
-                Notifications.success("Role removed from composite.");
-            });
+            }
+            $scope.selectedRealmMappings = [];
+            Notifications.success("Role removed from composite.");
+        });
     };
 
     $scope.addClientRole = function() {
         $scope.compositeSwitchDisabled=true;
         $http.post(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
-                $scope.selectedClientRoles).then(function() {
-                for (var i = 0; i < $scope.selectedClientRoles.length; i++) {
-                    var role = $scope.selectedClientRoles[i];
-                    var idx = $scope.clientRoles.indexOf($scope.selectedClientRoles[i]);
-                    if (idx != -1) {
-                        $scope.clientRoles.splice(idx, 1);
-                        $scope.clientMappings.push(role);
-                    }
+            $scope.selectedClientRoles).then(function() {
+            for (var i = 0; i < $scope.selectedClientRoles.length; i++) {
+                var role = $scope.selectedClientRoles[i];
+                var idx = $scope.clientRoles.indexOf($scope.selectedClientRoles[i]);
+                if (idx != -1) {
+                    $scope.clientRoles.splice(idx, 1);
+                    $scope.clientMappings.push(role);
                 }
-                $scope.selectedClientRoles = [];
-            });
+            }
+            $scope.selectedClientRoles = [];
+        });
     };
 
     $scope.deleteClientRole = function() {
         $scope.compositeSwitchDisabled=true;
         $http.delete(authUrl + '/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
             {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).then(function() {
-                for (var i = 0; i < $scope.selectedClientMappings.length; i++) {
-                    var role = $scope.selectedClientMappings[i];
-                    var idx = $scope.clientMappings.indexOf($scope.selectedClientMappings[i]);
-                    if (idx != -1) {
-                        $scope.clientMappings.splice(idx, 1);
-                        $scope.clientRoles.push(role);
-                    }
+            for (var i = 0; i < $scope.selectedClientMappings.length; i++) {
+                var role = $scope.selectedClientMappings[i];
+                var idx = $scope.clientMappings.indexOf($scope.selectedClientMappings[i]);
+                if (idx != -1) {
+                    $scope.clientMappings.splice(idx, 1);
+                    $scope.clientRoles.push(role);
                 }
-                $scope.selectedClientMappings = [];
-            });
+            }
+            $scope.selectedClientMappings = [];
+        });
     };
 
 
@@ -1073,10 +1073,10 @@ module.factory('ClientTestNodesAvailable', function($resource) {
 
 module.factory('ClientCertificate', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/clients/:client/certificates/:attribute', {
-            realm : '@realm',
-            client : "@client",
-            attribute: "@attribute"
-        });
+        realm : '@realm',
+        client : "@client",
+        attribute: "@attribute"
+    });
 });
 
 module.factory('ClientCertificateGenerate', function($resource) {
@@ -1094,10 +1094,10 @@ module.factory('ClientCertificateGenerate', function($resource) {
 
 module.factory('ClientCertificateDownload', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/clients/:client/certificates/:attribute/download', {
-        realm : '@realm',
-        client : "@client",
-        attribute: "@attribute"
-    },
+            realm : '@realm',
+            client : "@client",
+            attribute: "@attribute"
+        },
         {
             download : {
                 method : 'POST',
@@ -1161,9 +1161,9 @@ module.factory('ClientInstallationJBoss', function($resource) {
     var url = authUrl + '/admin/realms/:realm/clients/:client/installation/jboss';
     return {
         url : function(parameters)
-     {
-        return url.replace(':realm', parameters.realm).replace(':client', parameters.client);
-    }
+        {
+            return url.replace(':realm', parameters.realm).replace(':client', parameters.client);
+        }
     }
 });
 
@@ -1625,10 +1625,26 @@ module.factory('GroupChildren', function($resource) {
     });
 });
 
+module.factory('GroupsCount', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/groups/count', {
+            realm : '@realm'
+        },
+        {
+            query: {
+                isArray: false,
+                method: 'GET',
+                params: {},
+                transformResponse: function (data) {
+                    return angular.fromJson(data)
+                }
+            }
+        });
+});
+
 module.factory('Groups', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/groups', {
         realm : '@realm'
-    });
+    })
 });
 
 module.factory('GroupRealmRoleMapping', function($resource) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html
index efb62bf..7ad6f65 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html
@@ -1,37 +1,50 @@
- <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-     <kc-tabs-group-list></kc-tabs-group-list>
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <kc-tabs-group-list></kc-tabs-group-list>
 
-    <table class="table table-striped table-bordered">
-         <thead>
-         <tr>
-             <th class="kc-table-actions" colspan="5">
-                 <div class="form-inline">
-                      <div class="pull-right" data-ng-show="access.manageUsers">
-                          <button id="createGroup" class="btn btn-default" ng-click="createGroup(tree.currentNode)">{{:: 'new' | translate}}</button>
-                          <button id="editGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="edit(tree.currentNode)">{{:: 'edit' | translate}}</button>
-                          <button id="cutGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="cut(tree.currentNode)">{{:: 'cut' | translate}}</button>
-                          <button id="pasteGroup" ng-disabled="!cutNode" class="btn btn-default" ng-click="paste(tree.currentNode)">{{:: 'paste' | translate}}</button>
-                          <button id="removeGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="remove(tree.currentNode)">{{:: 'delete' | translate}}</button>
-                     </div>
-                 </div>
-             </th>
-         </tr>
-         </thead>
-         <tbody>
-         <tr>
-             <td>    <div
-                     tree-id="tree"
+    <table class="table table-striped table-bordered" style="margin-bottom: 0">
+        <thead>
+        <tr>
+            <th class="kc-table-actions" colspan="5">
+                <div class="form-inline">
+                    <div class="form-group">
+                        <div class="input-group">
+                            <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchTerms" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
+                            <div class="input-group-addon">
+                                <i class="fa fa-search" id="groupSearch" ng-click="searchGroup()"></i>
+                            </div>
+                        </div>
+                    </div>
+                    <button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
+                    <div class="pull-right" data-ng-show="access.manageUsers">
+                        <div class="form-inline">
+                            <button id="createGroup" class="btn btn-default" ng-click="createGroup(tree.currentNode)">{{:: 'new' | translate}}</button>
+                            <button id="editGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="edit(tree.currentNode)">{{:: 'edit' | translate}}</button>
+                            <button id="cutGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="cut(tree.currentNode)">{{:: 'cut' | translate}}</button>
+                            <button id="pasteGroup" ng-disabled="!cutNode" class="btn btn-default" ng-click="paste(tree.currentNode)">{{:: 'paste' | translate}}</button>
+                            <button id="removeGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="remove(tree.currentNode)">{{:: 'delete' | translate}}</button>
+                        </div>
+                    </div>
+                </div>
+            </th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr>
+            <td>
+                <div tree-id="tree"
                      angular-treeview="true"
                      tree-model="groupList"
                      node-id="id"
                      node-label="name"
                      node-children="subGroups" >
-             </div>
-             </td>
-         </tr>
-         </tbody>
-     </table>
-
- </div>
+                </div>
+            </td>
+        </tr>
+        </tbody>
+    </table>
+    <div style="margin-bottom: 50px">
+        <kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
+    </div>
+</div>
 
 <kc-menu></kc-menu>
\ No newline at end of file