keycloak-uncached

Changes

Details

diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
index f138d43..f00f942 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
@@ -57,6 +57,17 @@ public interface UserResource {
     @GET
     List<GroupRepresentation> groups();
 
+    @Path("groups")
+    @GET
+    List<GroupRepresentation> groups(@QueryParam("first") Integer firstResult,
+                                     @QueryParam("max") Integer maxResults);
+
+    @Path("groups")
+    @GET
+    List<GroupRepresentation> groups(@QueryParam("search") String search,
+                                     @QueryParam("first") Integer firstResult,
+                                     @QueryParam("max") Integer maxResults);
+
     @Path("groups/{groupId}")
     @PUT
     void joinGroup(@PathParam("groupId") String groupId);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java
index 3a70b67..22aa8a4 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java
@@ -26,6 +26,7 @@ import org.keycloak.models.cache.infinispan.DefaultLazyLoader;
 import org.keycloak.models.cache.infinispan.LazyLoader;
 
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -68,7 +69,7 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm 
         this.requiredActions = new DefaultLazyLoader<>(UserModel::getRequiredActions, Collections::emptySet);
         this.attributes = new DefaultLazyLoader<>(userModel -> new MultivaluedHashMap<>(userModel.getAttributes()), MultivaluedHashMap::new);
         this.roleMappings = new DefaultLazyLoader<>(userModel -> userModel.getRoleMappings().stream().map(RoleModel::getId).collect(Collectors.toSet()), Collections::emptySet);
-        this.groups = new DefaultLazyLoader<>(userModel -> userModel.getGroups().stream().map(GroupModel::getId).collect(Collectors.toSet()), Collections::emptySet);
+        this.groups = new DefaultLazyLoader<>(userModel -> userModel.getGroups().stream().map(GroupModel::getId).collect(Collectors.toCollection(LinkedHashSet::new)), LinkedHashSet::new);
     }
 
     public String getRealm() {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
index 63cfb65..9ee1138 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
@@ -31,6 +31,7 @@ import org.keycloak.models.utils.RoleUtils;
 
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -256,7 +257,7 @@ public class UserAdapter implements CachedUserModel {
     public void setFederationLink(String link) {
         getDelegateForUpdate();
         updated.setFederationLink(link);
-   }
+    }
 
     @Override
     public String getServiceAccountClientLink() {
@@ -346,7 +347,7 @@ public class UserAdapter implements CachedUserModel {
     @Override
     public Set<GroupModel> getGroups() {
         if (updated != null) return updated.getGroups();
-        Set<GroupModel> groups = new HashSet<GroupModel>();
+        Set<GroupModel> groups = new LinkedHashSet<>();
         for (String id : cached.getGroups(modelSupplier)) {
             GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm);
             if (groupModel == null) {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java
index 23024fb..ff492f0 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java
@@ -37,12 +37,10 @@ import java.io.Serializable;
         @NamedQuery(name="userMemberOf", query="select m from UserGroupMembershipEntity m where m.user = :user and m.groupId = :groupId"),
         @NamedQuery(name="userGroupMembership", query="select m from UserGroupMembershipEntity m where m.user = :user"),
         @NamedQuery(name="groupMembership", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId order by g.user.username"),
-        @NamedQuery(name="userGroupIds", query="select m.groupId from UserGroupMembershipEntity m where m.user = :user"),
         @NamedQuery(name="deleteUserGroupMembershipByRealm", query="delete from  UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId)"),
         @NamedQuery(name="deleteUserGroupMembershipsByRealmAndLink", query="delete from  UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
         @NamedQuery(name="deleteUserGroupMembershipsByGroup", query="delete from UserGroupMembershipEntity m where m.groupId = :groupId"),
         @NamedQuery(name="deleteUserGroupMembershipsByUser", query="delete from UserGroupMembershipEntity m where m.user = :user")
-
 })
 @Table(name="USER_GROUP_MEMBERSHIP")
 @Entity
@@ -55,6 +53,11 @@ public class UserGroupMembershipEntity {
     protected UserEntity user;
 
     @Id
+    @ManyToOne(fetch= FetchType.LAZY)
+    @JoinColumn(name="GROUP_ID", insertable=false, updatable=false)
+    protected GroupEntity group;
+
+    @Id
     @Column(name = "GROUP_ID")
     protected String groupId;
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index a80dec9..754229b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -25,6 +25,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleContainerModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.jpa.entities.GroupEntity;
 import org.keycloak.models.jpa.entities.UserAttributeEntity;
 import org.keycloak.models.jpa.entities.UserEntity;
 import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
@@ -36,12 +37,20 @@ import org.keycloak.models.utils.RoleUtils;
 import javax.persistence.EntityManager;
 import javax.persistence.Query;
 import javax.persistence.TypedQuery;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Join;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.Objects;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -289,23 +298,79 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
         user.setEmailVerified(verified);
     }
 
-    @Override
-    public Set<GroupModel> getGroups() {
+    private TypedQuery<String> createGetGroupsQuery(String search, Integer first, Integer max) {
         // we query ids only as the group  might be cached and following the @ManyToOne will result in a load
         // even if we're getting just the id.
-        TypedQuery<String> query = em.createNamedQuery("userGroupIds", String.class);
-        query.setParameter("user", getEntity());
-        List<String> ids = query.getResultList();
-        Set<GroupModel> groups = new HashSet<>();
-        for (String groupId : ids) {
-            GroupModel group = realm.getGroupById(groupId);
-            if (group == null) continue;
-            groups.add(group);
+        CriteriaBuilder builder = em.getCriteriaBuilder();
+        CriteriaQuery<String> queryBuilder = builder.createQuery(String.class);
+        Root<UserGroupMembershipEntity> root = queryBuilder.from(UserGroupMembershipEntity.class);
+
+        List<Predicate> predicates = new ArrayList<>();
+        predicates.add(builder.equal(root.get("user"), getEntity()));
+        Join<UserGroupMembershipEntity, GroupEntity> join = root.join("group");
+        if (Objects.nonNull(search) && !search.isEmpty()) {
+            predicates.add(builder.like(builder.lower(join.get("name")), builder.lower(builder.literal("%" + search + "%"))));
+        }
+
+        queryBuilder.select(root.get("groupId"));
+        queryBuilder.where(predicates.toArray(new Predicate[0]));
+        queryBuilder.orderBy(builder.asc(join.get("name")));
+
+        TypedQuery<String> query = em.createQuery(queryBuilder);
+        if (Objects.nonNull(first) && Objects.nonNull(max)) {
+            query.setFirstResult(first).setMaxResults(max);
+        }
+        return query;
+    }
+
+    private TypedQuery<Long> createCountGroupsQuery(String search) {
+        // we query ids only as the group  might be cached and following the @ManyToOne will result in a load
+        // even if we're getting just the id.
+        CriteriaBuilder builder = em.getCriteriaBuilder();
+        CriteriaQuery<Long> queryBuilder = builder.createQuery(Long.class);
+        Root<UserGroupMembershipEntity> root = queryBuilder.from(UserGroupMembershipEntity.class);
+
+        List<Predicate> predicates = new ArrayList<>();
+        predicates.add(builder.equal(root.get("user"), getEntity()));
+        if (Objects.nonNull(search) && !search.isEmpty()) {
+            Join<UserGroupMembershipEntity, GroupEntity> join = root.join("group");
+            predicates.add(builder.like(join.get("name"), builder.literal("%" + search + "%")));
+        }
+
+        queryBuilder.select(builder.count(root));
+        queryBuilder.where(predicates.toArray(new Predicate[0]));
+        return em.createQuery(queryBuilder);
+    }
+
+    private Set<GroupModel> getGroupModels(Collection<String> groupIds) {
+        Set<GroupModel> groups = new LinkedHashSet<>();
+        for (String id : groupIds) {
+            groups.add(realm.getGroupById(id));
         }
         return groups;
     }
 
     @Override
+    public Set<GroupModel> getGroups() {
+        return getGroupModels(createGetGroupsQuery(null, null, null).getResultList());
+    }
+
+    @Override
+    public Set<GroupModel> getGroups(String search, int first, int max) {
+        return getGroupModels(createGetGroupsQuery(search, first, max).getResultList());
+    }
+
+    @Override
+    public long getGroupsCount() {
+        return createCountGroupsQuery(null).getSingleResult();
+    }
+
+    @Override
+    public long getGroupsCountByNameContaining(String search) {
+        return createCountGroupsQuery(search).getSingleResult();
+    }
+
+    @Override
     public void joinGroup(GroupModel group) {
         if (isMemberOf(group)) return;
         joinGroupImpl(group);
@@ -354,7 +419,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
     public boolean hasRole(RoleModel role) {
         Set<RoleModel> roles = getRoleMappings();
         return RoleUtils.hasRole(roles, role)
-          || RoleUtils.hasRoleFromGroup(getGroups(), role, true);
+                || RoleUtils.hasRoleFromGroup(getGroups(), role, true);
     }
 
     protected TypedQuery<UserRoleMappingEntity> getUserRoleMappingEntityTypedQuery(RoleModel role) {
@@ -431,9 +496,9 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
         for (RoleModel role : roleMappings) {
             RoleContainerModel container = role.getContainer();
             if (container instanceof ClientModel) {
-                ClientModel appModel = (ClientModel)container;
+                ClientModel appModel = (ClientModel) container;
                 if (appModel.getId().equals(app.getId())) {
-                   roles.add(role);
+                    roles.add(role);
                 }
             }
         }
@@ -476,5 +541,4 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
     }
 
 
-
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java
index 3da6525..d1439a5 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java
@@ -19,9 +19,11 @@ package org.keycloak.models;
 
 import org.keycloak.provider.ProviderEvent;
 
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -112,6 +114,32 @@ public interface UserModel extends RoleMapperModel {
     void setEmailVerified(boolean verified);
 
     Set<GroupModel> getGroups();
+
+    default Set<GroupModel> getGroups(int first, int max) {
+        return getGroups(null, first, max);
+    }
+
+    default Set<GroupModel> getGroups(String search, int first, int max) {
+        return getGroups().stream()
+                .filter(group -> search == null || group.getName().toLowerCase().contains(search.toLowerCase()))
+                .skip(first)
+                .limit(max)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    default long getGroupsCount() {
+        return getGroupsCountByNameContaining(null);
+    }
+    
+    default long getGroupsCountByNameContaining(String search) {
+        if (search == null) {
+            return getGroups().size();
+        }
+
+        String s = search.toLowerCase();
+        return getGroups().stream().filter(group -> group.getName().toLowerCase().contains(s)).count();
+    }
+
     void joinGroup(GroupModel group);
     void leaveGroup(GroupModel group);
     boolean isMemberOf(GroupModel group);
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 33b026e..9f91721 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
@@ -24,7 +24,6 @@ import org.keycloak.authorization.model.Resource;
 import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.model.Scope;
 import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
-import org.keycloak.common.Profile;
 import org.keycloak.common.util.MultivaluedHashMap;
 import org.keycloak.common.util.Time;
 import org.keycloak.component.ComponentModel;
@@ -38,12 +37,17 @@ import org.keycloak.representations.idm.*;
 import org.keycloak.representations.idm.authorization.*;
 import org.keycloak.storage.StorageId;
 
-import java.util.*;
-import java.util.stream.Collectors;
+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.Objects;
 import java.util.Set;
-import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -102,6 +106,12 @@ public class ModelToRepresentation {
         return result;
     }
 
+    public static List<GroupRepresentation> searchForGroupByName(UserModel user, boolean full, String search, Integer first, Integer max) {
+        return user.getGroups(search, first, max).stream()
+                .map(group -> toRepresentation(group, full))
+                .collect(Collectors.toList());
+    }
+
     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);
@@ -113,6 +123,12 @@ public class ModelToRepresentation {
         return hierarchy;
     }
 
+    public static List<GroupRepresentation> toGroupHierarchy(UserModel user, boolean full, Integer first, Integer max) {
+        return user.getGroups(first, max).stream()
+                .map(group -> toRepresentation(group, full))
+                .collect(Collectors.toList());
+    }
+
     public static List<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full) {
         List<GroupRepresentation> hierarchy = new LinkedList<>();
         List<GroupModel> groups = realm.getTopLevelGroups();
@@ -124,6 +140,12 @@ public class ModelToRepresentation {
         return hierarchy;
     }
 
+    public static List<GroupRepresentation> toGroupHierarchy(UserModel user, boolean full) {
+        return user.getGroups().stream()
+                .map(group -> toRepresentation(group, full))
+                .collect(Collectors.toList());
+    }
+
     public static GroupRepresentation toGroupHierarchy(GroupModel group, boolean full) {
         GroupRepresentation rep = toRepresentation(group, full);
         List<GroupRepresentation> subGroups = new LinkedList<>();
@@ -360,7 +382,7 @@ public class ModelToRepresentation {
         }
 
         rep.setInternationalizationEnabled(realm.isInternationalizationEnabled());
-        if(realm.getSupportedLocales() != null){
+        if (realm.getSupportedLocales() != null) {
             rep.setSupportedLocales(new HashSet<String>());
             rep.getSupportedLocales().addAll(realm.getSupportedLocales());
         }
@@ -381,7 +403,7 @@ public class ModelToRepresentation {
         return rep;
     }
 
-     public static void exportGroups(RealmModel realm, RealmRepresentation rep) {
+    public static void exportGroups(RealmModel realm, RealmRepresentation rep) {
         List<GroupRepresentation> groups = toGroupHierarchy(realm, true);
         rep.setGroups(groups);
     }
@@ -442,7 +464,7 @@ public class ModelToRepresentation {
             rep.setEventsListeners(new LinkedList<>(realm.getEventsListeners()));
         }
 
-        if(realm.getEnabledEventTypes() != null) {
+        if (realm.getEnabledEventTypes() != null) {
             rep.setEnabledEventTypes(new LinkedList<>(realm.getEnabledEventTypes()));
         }
 
@@ -649,7 +671,7 @@ public class ModelToRepresentation {
         return consentRep;
     }
 
-    public static AuthenticationFlowRepresentation  toRepresentation(RealmModel realm, AuthenticationFlowModel model) {
+    public static AuthenticationFlowRepresentation toRepresentation(RealmModel realm, AuthenticationFlowModel model) {
         AuthenticationFlowRepresentation rep = new AuthenticationFlowRepresentation();
         rep.setId(model.getId());
         rep.setBuiltIn(model.isBuiltIn());
@@ -676,7 +698,7 @@ public class ModelToRepresentation {
         if (model.getFlowId() != null) {
             AuthenticationFlowModel flow = realm.getAuthenticationFlowById(model.getFlowId());
             rep.setFlowAlias(flow.getAlias());
-       }
+        }
         rep.setPriority(model.getPriority());
         rep.setRequirement(model.getRequirement().name());
         return rep;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index c069f81..d6c62cd 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -100,6 +100,7 @@ import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -741,13 +742,39 @@ public class UserResource {
     @Path("groups")
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
-    public List<GroupRepresentation> groupMembership() {
+    public List<GroupRepresentation> groupMembership(@QueryParam("search") String search,
+                                                     @QueryParam("first") Integer firstResult,
+                                                     @QueryParam("max") Integer maxResults) {
         auth.users().requireView(user);
-        List<GroupRepresentation> memberships = new LinkedList<>();
-        for (GroupModel group : user.getGroups()) {
-            memberships.add(ModelToRepresentation.toRepresentation(group, false));
+        List<GroupRepresentation> results;
+
+        if (Objects.nonNull(search) && Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) {
+            results = ModelToRepresentation.searchForGroupByName(user, false, search.trim(), firstResult, maxResults);
+        } else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) {
+            results = ModelToRepresentation.toGroupHierarchy(user, false, firstResult, maxResults);
+        } else {
+            results = ModelToRepresentation.toGroupHierarchy(user, false);
+        }
+
+        return results;
+    }
+
+    @GET
+    @NoCache
+    @Path("groups/count")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, Long> getGroupMembershipCount(@QueryParam("search") String search) {
+        auth.users().requireView(user);
+        Long results;
+
+        if (Objects.nonNull(search)) {
+            results = user.getGroupsCountByNameContaining(search);
+        } else {
+            results = user.getGroupsCount();
         }
-        return memberships;
+        Map<String, Long> map = new HashMap<>();
+        map.put("count", results);
+        return map;
     }
 
     @DELETE
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/user/UserGroupMembershipTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/user/UserGroupMembershipTest.java
new file mode 100755
index 0000000..73d9267
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/user/UserGroupMembershipTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.admin.user;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.events.admin.ResourceType;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.admin.AbstractAdminTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.federation.DummyUserFederationProvider;
+import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.AdminEventPaths;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.ws.rs.core.Response;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.testsuite.Assert.assertNames;
+
+/**
+ * @author <a href="mailto:volker.suschke@bosch-si.com">Volker Suschke</a>
+ * @author <a href="mailto:leon.graser@bosch-si.com">Leon Graser</a>
+ */
+public class UserGroupMembershipTest extends AbstractAdminTest {
+
+    @Deployment
+    public static WebArchive deploy() {
+        return RunOnServerDeployment.create(
+                AbstractAdminTest.class,
+                AbstractTestRealmKeycloakTest.class,
+                DummyUserFederationProviderFactory.class, DummyUserFederationProvider.class,
+                UserResource.class);
+    }
+
+    public String createUser() {
+        return createUser("user1", "user1@localhost");
+    }
+
+    public String createUser(String username, String email) {
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername(username);
+        user.setEmail(email);
+        user.setRequiredActions(Collections.emptyList());
+        user.setEnabled(true);
+
+        return createUser(user);
+    }
+
+    private String createUser(UserRepresentation userRep) {
+        Response response = realm.users().create(userRep);
+        String createdId = ApiUtil.getCreatedId(response);
+        response.close();
+
+        assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userResourcePath(createdId), userRep, ResourceType.USER);
+
+        getCleanup().addUserId(createdId);
+
+        return createdId;
+    }
+
+    @Test
+    public void verifyCreateUser() {
+        createUser();
+    }
+
+    private GroupRepresentation createGroup(RealmResource realm, GroupRepresentation group) {
+        Response response = realm.groups().add(group);
+        String groupId = ApiUtil.getCreatedId(response);
+        getCleanup().addGroupId(groupId);
+        response.close();
+
+        assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.groupPath(groupId), group, ResourceType.GROUP);
+
+        // Set ID to the original rep
+        group.setId(groupId);
+        return group;
+    }
+
+    @Test
+    public void groupMembershipPaginated() {
+        Response response = realm.users().create(UserBuilder.create().username("user-a").build());
+        String userId = ApiUtil.getCreatedId(response);
+        response.close();
+        assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userResourcePath(userId), ResourceType.USER);
+
+        for (int i = 1; i <= 10; i++) {
+            GroupRepresentation group = new GroupRepresentation();
+            group.setName("group-" + i);
+            String groupId = createGroup(realm, group).getId();
+            realm.users().get(userId).joinGroup(groupId);
+            assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userGroupPath(userId, groupId), group, ResourceType.GROUP_MEMBERSHIP);
+        }
+
+        List<GroupRepresentation> groups = realm.users().get(userId).groups(5, 6);
+        assertEquals(groups.size(), 5);
+        assertNames(groups, "group-5","group-6","group-7","group-8","group-9");
+    }
+
+    @Test
+    public void groupMembershipSearch() {
+        Response response = realm.users().create(UserBuilder.create().username("user-b").build());
+        String userId = ApiUtil.getCreatedId(response);
+        response.close();
+        assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userResourcePath(userId), ResourceType.USER);
+
+        for (int i = 1; i <= 10; i++) {
+            GroupRepresentation group = new GroupRepresentation();
+            group.setName("group-" + i);
+            String groupId = createGroup(realm, group).getId();
+            realm.users().get(userId).joinGroup(groupId);
+            assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userGroupPath(userId, groupId), group, ResourceType.GROUP_MEMBERSHIP);
+        }
+
+        List<GroupRepresentation> groups = realm.users().get(userId).groups("-3", 0, 10);
+        assertEquals(1, groups.size());
+        assertNames(groups, "group-3");
+
+        List<GroupRepresentation> groups2 = realm.users().get(userId).groups("1", 0, 10);
+        assertEquals(2, groups2.size());
+        assertNames(groups2, "group-1", "group-10");
+
+        List<GroupRepresentation> groups3 = realm.users().get(userId).groups("1", 2, 10);
+        assertEquals(0, groups3.size());
+
+        List<GroupRepresentation> groups4 = realm.users().get(userId).groups("gr", 2, 10);
+        assertEquals(8, groups4.size());
+
+        List<GroupRepresentation> groups5 = realm.users().get(userId).groups("Gr", 2, 10);
+        assertEquals(8, groups5.size());
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java
index beddc3a..b052c09 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java
@@ -128,6 +128,21 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
                 UserModel johnDb = session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm);
                 Set<GroupModel> johnDbGroups = johnDb.getGroups();
                 Assert.assertEquals(2, johnDbGroups.size());
+
+                Set<GroupModel> johnDbGroupsWithGr = johnDb.getGroups("Gr", 0, 10);
+                Assert.assertEquals(2, johnDbGroupsWithGr.size());
+
+                Set<GroupModel> johnDbGroupsWithGr2 = johnDb.getGroups("Gr", 1, 10);
+                Assert.assertEquals(1, johnDbGroupsWithGr2.size());
+
+                Set<GroupModel> johnDbGroupsWithGr3 = johnDb.getGroups("Gr", 0, 1);
+                Assert.assertEquals(1, johnDbGroupsWithGr3.size());
+
+                Set<GroupModel> johnDbGroupsWith12 = johnDb.getGroups("12", 0, 10);
+                Assert.assertEquals(1, johnDbGroupsWith12.size());
+
+                long dbGroupCount = johnDb.getGroupsCount();
+                Assert.assertEquals(2, dbGroupCount);
             });
         }
 
@@ -145,10 +160,24 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
 
             Set<GroupModel> johnGroups = john.getGroups();
             Assert.assertEquals(2, johnGroups.size());
+            long groupCount = john.getGroupsCount();
+            Assert.assertEquals(2, groupCount);
             Assert.assertTrue(johnGroups.contains(group1));
             Assert.assertFalse(johnGroups.contains(group11));
             Assert.assertTrue(johnGroups.contains(group12));
 
+            Set<GroupModel> johnGroupsWithGr = john.getGroups("gr", 0, 10);
+            Assert.assertEquals(2, johnGroupsWithGr.size());
+
+            Set<GroupModel> johnGroupsWithGr2 = john.getGroups("gr", 1, 10);
+            Assert.assertEquals(1, johnGroupsWithGr2.size());
+
+            Set<GroupModel> johnGroupsWithGr3 = john.getGroups("gr", 0, 1);
+            Assert.assertEquals(1, johnGroupsWithGr3.size());
+
+            Set<GroupModel> johnGroupsWith12 = john.getGroups("12", 0, 10);
+            Assert.assertEquals(1, johnGroupsWith12.size());
+
             // 4 - Check through userProvider
             List<UserModel> group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10);
             List<UserModel> group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10);
@@ -170,6 +199,9 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
 
             johnGroups = john.getGroups();
             Assert.assertEquals(0, johnGroups.size());
+            
+            groupCount = john.getGroupsCount();
+            Assert.assertEquals(0, groupCount);
         });
     }
 
@@ -211,6 +243,21 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
             Assert.assertTrue(maryGroups.contains(group1));
             Assert.assertTrue(maryGroups.contains(group11));
             Assert.assertTrue(maryGroups.contains(group12));
+
+            long groupCount = mary.getGroupsCount();
+            Assert.assertEquals(5, groupCount);
+
+            Set<GroupModel> maryGroupsWithGr = mary.getGroups("gr", 0, 10);
+            Assert.assertEquals(5, maryGroupsWithGr.size());
+
+            Set<GroupModel> maryGroupsWithGr2 = mary.getGroups("gr", 1, 10);
+            Assert.assertEquals(4, maryGroupsWithGr2.size());
+
+            Set<GroupModel> maryGroupsWithGr3 = mary.getGroups("gr", 0, 1);
+            Assert.assertEquals(1, maryGroupsWithGr3.size());
+
+            Set<GroupModel> maryGroupsWith12 = mary.getGroups("12", 0, 10);
+            Assert.assertEquals(2, maryGroupsWith12.size());
         });
 
         // Assert that access through DB will have just DB mapped groups
@@ -230,6 +277,21 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
                 Assert.assertFalse(maryDBGroups.contains(group11));
                 Assert.assertTrue(maryDBGroups.contains(group12));
 
+                Set<GroupModel> maryDBGroupsWithGr = maryDB.getGroups("Gr", 0, 10);
+                Assert.assertEquals(3, maryDBGroupsWithGr.size());
+
+                Set<GroupModel> maryDBGroupsWithGr2 = maryDB.getGroups("Gr", 1, 10);
+                Assert.assertEquals(2, maryDBGroupsWithGr2.size());
+
+                Set<GroupModel> maryDBGroupsWithGr3 = maryDB.getGroups("Gr", 0, 1);
+                Assert.assertEquals(1, maryDBGroupsWithGr3.size());
+
+                Set<GroupModel> maryDBGroupsWith12 = maryDB.getGroups("12", 0, 10);
+                Assert.assertEquals(2, maryDBGroupsWith12.size());
+
+                long dbGroupCount = maryDB.getGroupsCount();
+                Assert.assertEquals(3, dbGroupCount);
+
                 // Test the group mapping available for group12
                 List<UserModel> group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10);
                 Assert.assertEquals(1, group12Members.size());
@@ -321,6 +383,21 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
             Assert.assertTrue(robGroups.contains(group11));
             Assert.assertTrue(robGroups.contains(group12));
 
+            Set<GroupModel> robGroupsWithGr = rob.getGroups("Gr", 0, 10);
+            Assert.assertEquals(4, robGroupsWithGr.size());
+
+            Set<GroupModel> robGroupsWithGr2 = rob.getGroups("Gr", 1, 10);
+            Assert.assertEquals(3, robGroupsWithGr2.size());
+
+            Set<GroupModel> robGroupsWithGr3 = rob.getGroups("Gr", 0, 1);
+            Assert.assertEquals(1, robGroupsWithGr3.size());
+
+            Set<GroupModel> robGroupsWith12 = rob.getGroups("12", 0, 10);
+            Assert.assertEquals(2, robGroupsWith12.size());
+
+            long dbGroupCount = rob.getGroupsCount();
+            Assert.assertEquals(4, dbGroupCount);
+
             // Delete some group mappings in LDAP and check that it doesn't have any effect and user still has groups
             LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName("group11");
             groupMapper.deleteGroupMappingInLDAP(robLdap, ldapGroup);
@@ -510,6 +587,24 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
             Assert.assertTrue(groups.contains(group31));
             Assert.assertTrue(groups.contains(group32));
             Assert.assertTrue(groups.contains(group4));
+
+            long groupsCount = john.getGroupsCount();
+            Assert.assertEquals(4, groupsCount);
+
+            Set<GroupModel> groupsWith3v1 = john.getGroups("3", 0, 10);
+            Assert.assertEquals(2, groupsWith3v1.size());
+
+            Set<GroupModel> groupsWith3v2 = john.getGroups("3", 1, 10);
+            Assert.assertEquals(1, groupsWith3v2.size());
+
+            Set<GroupModel> groupsWith3v3 = john.getGroups("3", 1, 1);
+            Assert.assertEquals(1, groupsWith3v3.size());
+
+            Set<GroupModel> groupsWith3v4 = john.getGroups("3", 1, 0);
+            Assert.assertEquals(0, groupsWith3v4.size());
+
+            Set<GroupModel> groupsWithKeycloak = john.getGroups("Keycloak", 0, 10);
+            Assert.assertEquals(0, groupsWithKeycloak.size());
         });
     }
 
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 02f1e4a..70306b5 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
@@ -657,9 +657,6 @@ module.config([ '$routeProvider', function($routeProvider) {
                 },
                 user : function(UserLoader) {
                     return UserLoader();
-                },
-                groups : function(GroupListLoader) {
-                    return GroupListLoader();
                 }
             },
             controller : 'UserGroupMembershipCtrl'
@@ -910,9 +907,6 @@ module.config([ '$routeProvider', function($routeProvider) {
             resolve : {
                 realm : function(RealmLoader) {
                     return RealmLoader();
-                },
-                groups : function(GroupListLoader) {
-                    return GroupListLoader();
                 }
             },
             controller : 'DefaultGroupsCtrl'
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 81af75d..63de108 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
@@ -8,11 +8,11 @@ module.controller('GroupListCtrl', function($scope, $route, $q, realm, Groups, G
         }
     ];
 
-
-    $scope.searchTerms = '';
+    $scope.searchCriteria = '';
     $scope.currentPage = 1;
     $scope.currentPageInput = $scope.currentPage;
     $scope.pageSize = 20;
+    $scope.numberOfPages = 1;
     $scope.tree = [];
 
     var refreshGroups = function (search) {
@@ -41,9 +41,7 @@ module.controller('GroupListCtrl', function($scope, $route, $q, realm, Groups, G
         }, function() {
             promiseGetGroups.reject('Unable to fetch ' + queryParams);
         });
-        var promiseGetGroupsChain   = promiseGetGroups.promise.then(function(groups) {
-            console.log('*** group call groups size: ' + groups.length);
-            console.log('*** group call groups size: ' + groups.length);
+        promiseGetGroups.promise.then(function(groups) {
             $scope.groupList = [
                 {
                     "id" : "realm",
@@ -51,6 +49,8 @@ module.controller('GroupListCtrl', function($scope, $route, $q, realm, Groups, G
                     "subGroups" : groups
                 }
             ];
+        }, function (failed) {
+            Notifications.error(failed);
         });
 
         var promiseCount = $q.defer();
@@ -60,27 +60,33 @@ module.controller('GroupListCtrl', function($scope, $route, $q, realm, Groups, G
         }, function() {
             promiseCount.reject('Unable to fetch ' + countParams);
         });
-        var promiseCountChain   = promiseCount.promise.then(function(groupsCount) {
-            $scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize);
-         });
+        promiseCount.promise.then(function(entry) {
+            if(angular.isDefined(entry.count) && entry.count > $scope.pageSize) {
+                $scope.numberOfPages = Math.ceil(entry.count/$scope.pageSize);
+            } else {
+                $scope.numberOfPages = 1;
+            }
+        }, function (failed) {
+            Notifications.error(failed);
+        });
     };
     refreshGroups();
 
     $scope.$watch('currentPage', function(newValue, oldValue) {
         if(newValue !== oldValue) {
-            refreshGroups($scope.searchTerms);
+            refreshGroups($scope.searchCriteria);
         }
     });
 
     $scope.clearSearch = function() {
-        $scope.searchTerms = '';
+        $scope.searchCriteria = '';
         $scope.currentPage = 1;
         refreshGroups();
     };
 
     $scope.searchGroup = function() {
         $scope.currentPage = 1;
-        refreshGroups($scope.searchTerms);
+        refreshGroups($scope.searchCriteria);
     };
 
     $scope.edit = function(selected) {
@@ -442,17 +448,89 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
 
 });
 
-module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications) {
+module.controller('DefaultGroupsCtrl', function($scope, $q, realm, Groups, GroupsCount, DefaultGroups, Notifications) {
     $scope.realm = realm;
-    $scope.groupList = groups;
+    $scope.groupList = [];
     $scope.selectedGroup = null;
     $scope.tree = [];
 
-    DefaultGroups.query({realm: realm.realm}, function(data) {
-        $scope.defaultGroups = data;
+    $scope.searchCriteria = '';
+    $scope.currentPage = 1;
+    $scope.currentPageInput = $scope.currentPage;
+    $scope.pageSize = 20;
+    $scope.numberOfPages = 1;
+
+    var refreshDefaultGroups = function () {
+        DefaultGroups.query({realm: realm.realm}, function(data) {
+            $scope.defaultGroups = data;
+        });
+    }
+
+    var refreshAvailableGroups = function (search) {
+        var first = ($scope.currentPage * $scope.pageSize) - $scope.pageSize;
+        var queryParams = {
+            realm : realm.id,
+            first : first,
+            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);
+        });
+        promiseGetGroups.promise.then(function(groups) {
+            $scope.groupList = groups;
+        }, function (failed) {
+            Notifications.success(failed);
+        });
+
+        var promiseCount = $q.defer();
+        GroupsCount.query(countParams, function(entry) {
+            promiseCount.resolve(entry);
+        }, function() {
+            promiseCount.reject('Unable to fetch ' + countParams);
+        });
+        promiseCount.promise.then(function(entry) {
+            if(angular.isDefined(entry.count) && entry.count > $scope.pageSize) {
+                $scope.numberOfPages = Math.ceil(entry.count/$scope.pageSize);
+            }
+        }, function (failed) {
+            Notifications.success(failed);
+        });
+    };
 
+    refreshAvailableGroups();
+
+    $scope.$watch('currentPage', function(newValue, oldValue) {
+        if(newValue !== oldValue) {
+            refreshAvailableGroups($scope.searchCriteria);
+        }
     });
 
+    $scope.clearSearch = function() {
+        $scope.searchCriteria = '';
+        $scope.currentPage = 1;
+        refreshAvailableGroups();
+    };
+
+    $scope.searchGroup = function() {
+        $scope.currentPage = 1;
+        refreshAvailableGroups($scope.searchCriteria);
+    };
+
+    refreshDefaultGroups();
+
     $scope.addDefaultGroup = function() {
         if (!$scope.tree.currentNode) {
             Notifications.error('Please select a group to add');
@@ -460,16 +538,16 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
         }
 
         DefaultGroups.update({realm: realm.realm, groupId: $scope.tree.currentNode.id}, function() {
+            refreshDefaultGroups();
             Notifications.success('Added default group');
-            $route.reload();
         });
 
     };
 
     $scope.removeDefaultGroup = function() {
         DefaultGroups.remove({realm: realm.realm, groupId: $scope.selectedGroup.id}, function() {
+            refreshDefaultGroups();
             Notifications.success('Removed default group');
-            $route.reload();
         });
 
     };
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index c51b9d5..8846754 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -929,56 +929,244 @@ function removeGroupMember(groups, member) {
         }
     }
 }
-module.controller('UserGroupMembershipCtrl', function($scope, $route, realm, groups, user, UserGroupMembership, UserGroupMapping, Notifications, $location, Dialog) {
+
+module.controller('UserGroupMembershipCtrl', function($scope, $q, realm, user, UserGroupMembership, UserGroupMembershipCount, UserGroupMapping, Notifications, Groups, GroupsCount) {
     $scope.realm = realm;
     $scope.user = user;
-    $scope.groupList = groups;
-    $scope.selectedGroup = null;
+    $scope.groupList = [];
+    $scope.allGroupMemberships = [];
+    $scope.groupMemberships = [];
     $scope.tree = [];
+    $scope.membershipTree = [];
+
+    $scope.searchCriteria = '';
+    $scope.searchCriteriaMembership = '';
+    $scope.currentPage = 1;
+    $scope.currentMembershipPage = 1;
+    $scope.currentPageInput = $scope.currentPage;
+    $scope.currentMembershipPageInput = $scope.currentMembershipPage;
+    $scope.pageSize = 20;
+    $scope.numberOfPages = 1;
+    $scope.numberOfMembershipPages = 1;
+
+    var refreshCompleteUserGroupMembership = function() {
+        var queryParams = {
+            realm : realm.realm,
+            userId: user.id
+        };
 
-    UserGroupMembership.query({realm: realm.realm, userId: user.id}, function(data) {
-        $scope.groupMemberships = data;
-        for (var i = 0; i < data.length; i++) {
-            var member = data[i];
-            removeGroupMember(groups, member);
+        var promiseGetCompleteUserGroupMembership = $q.defer();
+        UserGroupMembership.query(queryParams, function(entry) {
+            promiseGetCompleteUserGroupMembership.resolve(entry);
+        }, function() {
+            promiseGetCompleteUserGroupMembership.reject('Unable to fetch all group memberships' + queryParams);
+        });
+        promiseGetCompleteUserGroupMembership.promise.then(function(groups) {
+            for (var i = 0; i < groups.length; i++) {
+                $scope.allGroupMemberships.push(groups[i]);
+                $scope.getGroupClass(groups[i]);
+            }
+        }, function (failed) {
+            Notifications.error(failed);
+        });
+    };
+
+    var refreshUserGroupMembership = function (search) {
+        var first = ($scope.currentMembershipPage * $scope.pageSize) - $scope.pageSize;
+        var queryParams = {
+            realm : realm.realm,
+            userId: user.id,
+            first : first,
+            max : $scope.pageSize
+        };
+
+        var countParams = {
+            realm : realm.realm,
+            userId: user.id
+        };
+
+        var isSearch = function() {
+            return angular.isDefined(search) && search !== '';
+        };
+
+        if (isSearch()) {
+            queryParams.search = search;
+            countParams.search = search;
         }
 
+        var promiseGetUserGroupMembership = $q.defer();
+        UserGroupMembership.query(queryParams, function(entry) {
+            promiseGetUserGroupMembership.resolve(entry);
+        }, function() {
+            promiseGetUserGroupMembership.reject('Unable to fetch ' + queryParams);
+        });
+        promiseGetUserGroupMembership.promise.then(function(groups) {
+            $scope.groupMemberships = groups;
+        }, function (failed) {
+            Notifications.error(failed);
+        });
+
+        var promiseMembershipCount = $q.defer();
+        UserGroupMembershipCount.query(countParams, function(entry) {
+            promiseMembershipCount.resolve(entry);
+        }, function() {
+            promiseMembershipCount.reject('Unable to fetch ' + countParams);
+        });
+        promiseMembershipCount.promise.then(function(membershipEntry) {
+            if(angular.isDefined(membershipEntry.count) && membershipEntry.count > $scope.pageSize) {
+                $scope.numberOfMembershipPages = Math.ceil(membershipEntry.count/$scope.pageSize);
+            } else {
+                $scope.numberOfMembershipPages = 1;
+            }
+        }, function (failed) {
+            Notifications.error(failed);
+        });
+    };
+
+    var refreshAvailableGroups = function (search) {
+        var first = ($scope.currentPage * $scope.pageSize) - $scope.pageSize;
+        var queryParams = {
+            realm : realm.id,
+            first : first,
+            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);
+        });
+
+        promiseGetGroups.promise.then(function(groups) {
+            $scope.groupList = groups;
+        }, function (failed) {
+            Notifications.error(failed);
+        });
+
+        var promiseCount = $q.defer();
+        GroupsCount.query(countParams, function(entry) {
+            promiseCount.resolve(entry);
+        }, function() {
+            promiseCount.reject('Unable to fetch ' + countParams);
+        });
+        promiseCount.promise.then(function(entry) {
+            if(angular.isDefined(entry.count) && entry.count > $scope.pageSize) {
+                $scope.numberOfPages = Math.ceil(entry.count/$scope.pageSize);
+            } else {
+                $scope.numberOfPages = 1;
+            }
+        }, function (failed) {
+            Notifications.error(failed);
+        });
+        return promiseGetGroups.promise;
+    };
+
+    $scope.clearSearchMembership = function() {
+        $scope.searchCriteriaMembership = '';
+        $scope.currentMembershipPage = 1;
+        $scope.currentMembershipPageInput = 1;
+        refreshUserGroupMembership();
+    };
+
+    $scope.searchGroupMembership = function() {
+        $scope.currentMembershipPage = 1;
+        refreshUserGroupMembership($scope.searchCriteriaMembership);
+    };
+
+    refreshAvailableGroups();
+    refreshUserGroupMembership();
+    refreshCompleteUserGroupMembership();
+
+    $scope.$watch('currentPage', function(newValue, oldValue) {
+        if(newValue !== oldValue) {
+            refreshAvailableGroups($scope.searchCriteria)
+            .then(function(){
+                refreshUserGroupMembership($scope.searchCriteriaMembership);
+            });
+        }
+    });
+
+    $scope.$watch('currentMembershipPage', function(newValue, oldValue) {
+        if(newValue !== oldValue) {
+            refreshUserGroupMembership($scope.searchCriteriaMembership);
+        }
     });
 
+    $scope.clearSearch = function() {
+        $scope.searchCriteria = '';
+        $scope.currentPage = 1;
+        $scope.currentPageInput = 1;
+        refreshAvailableGroups();
+    };
 
+    $scope.searchGroup = function() {
+        $scope.currentPage = 1;
+        refreshAvailableGroups($scope.searchCriteria);
+    };
 
     $scope.joinGroup = function() {
         if (!$scope.tree.currentNode) {
             Notifications.error('Please select a group to add');
             return;
-        };
+        }
+        if (isMember($scope.tree.currentNode)) {
+            Notifications.error('Group already added');
+            return;
+        }
         UserGroupMapping.update({realm: realm.realm, userId: user.id, groupId: $scope.tree.currentNode.id}, function() {
+            $scope.allGroupMemberships.push($scope.tree.currentNode);
+            refreshUserGroupMembership();
             Notifications.success('Added group membership');
-            $route.reload();
         });
 
     };
 
     $scope.leaveGroup = function() {
-        if (!$scope.selectedGroup) {
+        if (!$scope.membershipTree.currentNode) {
+            Notifications.error('Please select a group to remove');
             return;
-
         }
-        UserGroupMapping.remove({realm: realm.realm, userId: user.id, groupId: $scope.selectedGroup.id}, function() {
+        UserGroupMapping.remove({realm: realm.realm, userId: user.id, groupId: $scope.membershipTree.currentNode.id}, function () {
+            removeGroupMember($scope.allGroupMemberships, $scope.membershipTree.currentNode);
+            refreshAvailableGroups();
+            refreshUserGroupMembership();
             Notifications.success('Removed group membership');
-            $route.reload();
         });
 
     };
 
     var isLeaf = function(node) {
-        return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
+        return node.id !== 'realm' && (!node.subGroups || node.subGroups.length === 0);
+    };
+
+    var isMember = function(node) {
+        for (var i = 0; i < $scope.allGroupMemberships.length; i++) {
+            var member = $scope.allGroupMemberships[i];
+            if (node.id === member.id) {
+                return true;
+            }
+        }
+        return false;
     };
 
     $scope.getGroupClass = function(node) {
         if (node.id == "realm") {
             return 'pficon pficon-users';
         }
+        if (isMember(node)) {
+            return 'normal deactivate';
+        }
         if (isLeaf(node)) {
             return 'normal';
         }
@@ -990,8 +1178,12 @@ module.controller('UserGroupMembershipCtrl', function($scope, $route, realm, gro
 
     $scope.getSelectedClass = function(node) {
         if (node.selected) {
-            return 'selected';
-        } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+            if (isMember(node)) {
+                return "deactivate_selected";
+            } else {
+                return 'selected';
+            }
+        } 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/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index e63a200..3598a27 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
@@ -1943,5 +1943,20 @@ module.factory('LDAPMapperSync', function($resource) {
 });
 
 
-
+module.factory('UserGroupMembershipCount', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/users/:userId/groups/count', {
+            realm : '@realm',
+            userId : '@userId'
+        },
+        {
+            query: {
+                isArray: false,
+                method: 'GET',
+                params: {},
+                transformResponse: function (data) {
+                    return angular.fromJson(data)
+                }
+            }
+        });
+});
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html b/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html
index 634944e..8d0866e 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html
@@ -1,80 +1,91 @@
- <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>
 
-     <form class="form-horizontal" name="realmForm" novalidate>
-         <div class="form-group" kc-read-only="!access.manageRealm">
-             <label class="col-md-1 control-label" class="control-label"></label>
+    <form class="form-horizontal" name="realmForm" novalidate>
+        <div class="form-group" kc-read-only="!access.manageRealm">
+            <label class="col-md-1 control-label" class="control-label"></label>
 
-             <div class="col-md-8" >
-                  <div class="row">
-                     <div class="col-md-5">
-                         <table class="table table-striped table-bordered">
-                             <thead>
-                             <tr>
-                                 <th class="kc-table-actions" colspan="5">
-                                     <div class="form-inline">
-                                         <label class="control-label">{{:: 'default-groups' | translate}}</label>
-                                         <kc-tooltip>{{:: 'default-groups.tooltip' | translate}}</kc-tooltip>
+            <div class="col-md-8" >
+                <div class="row">
+                    <div class="col-md-5">
+                        <table class="table table-striped table-bordered">
+                            <thead>
+                            <tr>
+                                <th class="kc-table-actions" colspan="5">
+                                    <div class="form-inline">
+                                        <label class="control-label">{{:: 'default-groups' | translate}}</label>
+                                        <kc-tooltip>{{:: 'default-groups.tooltip' | translate}}</kc-tooltip>
 
-                                         <div class="pull-right" data-ng-show="access.manageRealm">
-                                             <button id="removeDefaultGroup" class="btn btn-default" ng-click="removeDefaultGroup()">{{:: 'remove' | translate}}</button>
-                                         </div>
-                                     </div>
-                                 </th>
-                             </tr>
-                             </thead>
-                             <tbody>
-                             <tr>
-                                 <td>
-                                     <select id="defaultGroups" class="form-control" size=5
-                                                            ng-model="selectedGroup"
-                                                            ng-options="r.path for r in defaultGroups">
-                                         <option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
-                                 </select>
+                                        <div class="pull-right" data-ng-show="access.manageRealm">
+                                            <button id="removeDefaultGroup" class="btn btn-default" ng-click="removeDefaultGroup()">{{:: 'remove' | translate}}</button>
+                                        </div>
+                                    </div>
+                                </th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            <tr>
+                                <td>
+                                    <select id="defaultGroups" class="form-control" size=5
+                                            ng-model="selectedGroup"
+                                            ng-options="r.path for r in defaultGroups">
+                                        <option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
+                                    </select>
+                                </td>
+                            </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                    <div class="col-md-5">
+                        <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>
+                                            <label class="control-label">{{:: 'available-groups' | translate}}</label>
+                                            <kc-tooltip>{{:: 'available-groups.tooltip' | translate}}</kc-tooltip>
+                                        </div>
+                                        <div class="pull-left">
+                                            <div class="input-group">
+                                                <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteria" 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>
+                                        &nbsp;
+                                        <button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
+                                        <div class="pull-right" data-ng-show="access.manageRealm">
+                                            <button id="addDefaultGroup" class="btn btn-default" ng-click="addDefaultGroup()">{{:: 'add' | translate}}</button>
+                                        </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 style="margin-bottom: 50px">
+                            <kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+</div>
 
-
-                                 </td>
-                             </tr>
-                             </tbody>
-                         </table>
-                     </div>
-                     <div class="col-md-5">
-                         <table class="table table-striped table-bordered">
-                             <thead>
-                             <tr>
-                                 <th class="kc-table-actions" colspan="5">
-
-                                     <div class="form-inline">
-                                         <label class="control-label">{{:: 'available-groups' | translate}}</label>
-                                         <kc-tooltip>{{:: 'available-groups.tooltip' | translate}}</kc-tooltip>
-
-                                         <div class="pull-right" data-ng-show="access.manageRealm">
-                                             <button id="addDefaultGroup" class="btn btn-default" ng-click="addDefaultGroup()">{{:: 'add' | translate}}</button>
-                                         </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>
-             </div>
-         </div>
-     </form>
- </div>
-
-<kc-menu></kc-menu>
\ No newline at end of file
+<kc-menu></kc-menu>
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 7ad6f65..97a568d 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
@@ -8,7 +8,7 @@
                 <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()">
+                            <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteria" 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>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html
index f7bf04e..7504108 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html
@@ -13,14 +13,25 @@
              <div class="col-md-8" >
                   <div class="row">
                      <div class="col-md-5">
-                         <table class="table table-striped table-bordered">
+                         <table class="table table-striped table-bordered" style="margin-bottom: 0">
                              <thead>
                              <tr>
                                  <th class="kc-table-actions" colspan="5">
                                      <div class="form-inline">
-                                         <label class="control-label">{{:: 'group-membership' | translate}}</label>
-                                         <kc-tooltip>{{:: 'group-membership.tooltip' | translate}}</kc-tooltip>
-
+                                         <div>
+                                            <label class="control-label">{{:: 'group-membership' | translate}}</label>
+                                            <kc-tooltip>{{:: 'group-membership.tooltip' | translate}}</kc-tooltip>
+                                         </div>
+                                         <div class="pull-left">
+                                             <div class="input-group">
+                                                 <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteriaMembership" 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="searchGroupMembership()"></i>
+                                                 </div>
+                                             </div>
+                                         </div>
+                                         &nbsp;
+                                         <button id="viewAllGroups" class="btn btn-default" ng-click="clearSearchMembership()">{{:: 'view-all-groups' | translate}}</button>
                                          <div class="pull-right" data-ng-show="user.access.manageGroupMembership">
                                              <button id="leaveGroups" class="btn btn-default" ng-click="leaveGroup()">{{:: 'leave' | translate}}</button>
                                          </div>
@@ -31,28 +42,43 @@
                              <tbody>
                              <tr>
                                  <td>
-                                     <select id="groupMembership" class="form-control" size=5
-                                                            ng-model="selectedGroup"
-                                                            ng-options="r.path for r in groupMemberships">
-                                         <option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
-                                 </select>
-
-
+                                     <div
+                                             tree-id="membershipTree"
+                                             angular-treeview="true"
+                                             tree-model="groupMemberships"
+                                             node-id="id"
+                                             node-label="name"
+                                             node-children="subGroupsMembership" >
+                                     </div>
                                  </td>
                              </tr>
                              </tbody>
                          </table>
+                         <div style="margin-bottom: 50px">
+                             <kc-paging current-page="currentMembershipPage" number-of-pages="numberOfMembershipPages" current-page-input="currentMembershipPageInput"></kc-paging>
+                         </div>
                      </div>
                      <div class="col-md-5">
-                         <table class="table table-striped table-bordered">
+                         <table class="table table-striped table-bordered" style="margin-bottom: 0">
                              <thead>
                              <tr>
                                  <th class="kc-table-actions" colspan="5">
 
                                      <div class="form-inline">
-                                         <label class="control-label">{{:: 'available-groups' | translate}}</label>
-                                         <kc-tooltip>{{:: 'membership.available-groups.tooltip' | translate}}</kc-tooltip>
-
+                                         <div>
+                                             <label class="control-label">{{:: 'available-groups' | translate}}</label>
+                                             <kc-tooltip>{{:: 'membership.available-groups.tooltip' | translate}}</kc-tooltip>
+                                         </div>
+                                         <div class="pull-left">
+                                             <div class="input-group">
+                                                 <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteria" 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_availablegroups" ng-click="searchGroup()"></i>
+                                                 </div>
+                                             </div>
+                                         </div>
+                                         &nbsp;
+                                         <button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
                                          <div class="pull-right" data-ng-show="user.access.manageGroupMembership">
                                              <button id="joinGroup" class="btn btn-default" ng-click="joinGroup()">{{:: 'join' | translate}}</button>
                                          </div>
@@ -60,21 +86,24 @@
                                  </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>
+                                 <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 style="margin-bottom: 50px">
+                             <kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
+                         </div>
                      </div>
                  </div>
              </div>
diff --git a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
index 8b0a4d3..1fcd20d 100755
--- a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
+++ b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
@@ -404,4 +404,17 @@ table.kc-authz-table-expanded {
 .password-conceal {
   font-family: 'text-security-disc';
   font-size: 14px;
-}
\ No newline at end of file
+}
+
+/* Deactivation styles for user-group membership tree models */
+
+div[tree-model] li .deactivate {
+    color: #4a5053;
+    opacity: 0.4;
+}
+
+div[tree-model] li .deactivate_selected {
+    background-color: #dcdcdc;
+    font-weight: bold;
+    padding: 1px 5px;
+}
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css b/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css
index dee6898..3915d8c 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css
@@ -654,3 +654,4 @@ ol.setup-message li ul li {
   margin-left: 24px;
   margin-bottom: 8px;
 }
+