From c4da7637d6177d07e9a293656d8cfa7816da729e Mon Sep 17 00:00:00 2001 From: Levente NAGY Date: Tue, 6 Jun 2017 18:32:48 +0200 Subject: [PATCH 1/7] [KEYCLOAK-2538] - groups pagination and group search --- .../admin/client/resource/GroupsResource.java | 39 ++++++-- .../models/cache/infinispan/RealmAdapter.java | 42 ++++----- .../cache/infinispan/RealmCacheSession.java | 84 +++++++++-------- .../keycloak/models/jpa/JpaRealmProvider.java | 57 ++++++++---- .../org/keycloak/models/jpa/RealmAdapter.java | 60 +++--------- .../models/jpa/entities/GroupEntity.java | 16 +--- .../models/cache/CachedRealmModel.java | 1 - .../models/utils/ModelToRepresentation.java | 92 ++++++------------- .../java/org/keycloak/models/RealmModel.java | 12 +-- .../org/keycloak/models/RealmProvider.java | 6 +- .../resources/admin/GroupsResource.java | 28 ++++-- 11 files changed, 206 insertions(+), 231 deletions(-) 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 78070941ad..ce6db747fc 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java @@ -20,12 +20,7 @@ package org.keycloak.admin.client.resource; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.representations.idm.GroupRepresentation; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; @@ -35,11 +30,43 @@ import java.util.List; * @version $Revision: 1 $ */ public interface GroupsResource { + + /** + * Get all groups. + * @return A list containing all groups. + */ @GET @NoCache @Produces(MediaType.APPLICATION_JSON) List 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 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 groups(@QueryParam("search") String search, + @QueryParam("first") Integer first, + @QueryParam("max") Integer max); + /** * create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent * if the group doesn't exist. diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 0bed8262a1..d39646305d 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 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()); @@ -1225,6 +1205,16 @@ public class RealmAdapter implements CachedRealmModel { return cacheSession.getTopLevelGroups(this); } + @Override + public List getTopLevelGroups(Integer first, Integer max) { + return cacheSession.getTopLevelGroups(this, first, max); + } + + @Override + public List 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 52c1309384..47267ada62 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -19,50 +19,15 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; -import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.migration.MigrationModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientTemplateModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakTransaction; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RealmProvider; -import org.keycloak.models.RoleModel; +import org.keycloak.models.*; import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.CachedRealmModel; -import org.keycloak.models.cache.infinispan.entities.CachedClient; -import org.keycloak.models.cache.infinispan.entities.CachedClientRole; -import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate; -import org.keycloak.models.cache.infinispan.entities.CachedGroup; -import org.keycloak.models.cache.infinispan.entities.CachedRealm; -import org.keycloak.models.cache.infinispan.entities.CachedRealmRole; -import org.keycloak.models.cache.infinispan.entities.CachedRole; -import org.keycloak.models.cache.infinispan.entities.ClientListQuery; -import org.keycloak.models.cache.infinispan.entities.GroupListQuery; -import org.keycloak.models.cache.infinispan.entities.RealmListQuery; -import org.keycloak.models.cache.infinispan.entities.RoleListQuery; -import org.keycloak.models.cache.infinispan.events.ClientAddedEvent; -import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent; -import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent; -import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent; -import org.keycloak.models.cache.infinispan.events.GroupAddedEvent; -import org.keycloak.models.cache.infinispan.events.GroupMovedEvent; -import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent; -import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent; -import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent; -import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent; -import org.keycloak.models.cache.infinispan.events.RoleAddedEvent; -import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent; -import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent; +import org.keycloak.models.cache.infinispan.entities.*; +import org.keycloak.models.cache.infinispan.events.*; import org.keycloak.models.utils.KeycloakModelUtils; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** @@ -912,6 +877,47 @@ public class RealmCacheSession implements CacheRealmProvider { return list; } + @Override + public List 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 model = getDelegate().getTopLevelGroups(realm, first, max); + if (model == null) return null; + Set 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 list = new LinkedList<>(); + for (String id : query.getGroups()) { + GroupModel group = session.realms().getGroupById(id, realm); + if (Objects.isNull(group)) { + invalidations.add(cacheKey); + return getDelegate().getTopLevelGroups(realm); + } + list.add(group); + } + return list; + } + + @Override + public List 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); 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 718d19a03c..683f7a31e6 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -20,29 +20,13 @@ package org.keycloak.models.jpa; import org.jboss.logging.Logger; import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.migration.MigrationModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientTemplateModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RealmProvider; -import org.keycloak.models.RoleContainerModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.jpa.entities.ClientEntity; -import org.keycloak.models.jpa.entities.ClientTemplateEntity; -import org.keycloak.models.jpa.entities.GroupEntity; -import org.keycloak.models.jpa.entities.RealmEntity; -import org.keycloak.models.jpa.entities.RoleEntity; +import org.keycloak.models.*; +import org.keycloak.models.jpa.entities.*; import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** @@ -349,6 +333,24 @@ public class JpaRealmProvider implements RealmProvider { Collectors.toList(), Collections::unmodifiableList)); } + @Override + public List getTopLevelGroups(RealmModel realm, Integer first, Integer max) { + List groupIds = em.createNamedQuery("getTopLevelGroupIds", String.class) + .setParameter("realm", realm.getId()) + .setFirstResult(first) + .setMaxResults(max) + .getResultList(); + List list = new ArrayList<>(); + if(Objects.nonNull(groupIds) && !groupIds.isEmpty()) { + for (String id : groupIds) { + GroupModel group = getGroupById(id, realm); + list.add(group); + } + } + + return Collections.unmodifiableList(list); + } + @Override public boolean removeGroup(RealmModel realm, GroupModel group) { if (group == null) { @@ -519,4 +521,21 @@ public class JpaRealmProvider implements RealmProvider { ClientTemplateAdapter adapter = new ClientTemplateAdapter(realm, em, session, app); return adapter; } + + @Override + public List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { + TypedQuery 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 groups = query.getResultList(); + if (Objects.isNull(groups)) return Collections.EMPTY_LIST; + List list = new LinkedList<>(); + for (String id : groups) { + list.add(session.realms().getGroupById(id, realm)); + } + return Collections.unmodifiableList(list); + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 33ca943de6..e53ef2f6b6 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 { realm.setVerifyEmail(verifyEmail); em.flush(); } - + @Override public boolean isLoginWithEmailAllowed() { return realm.isLoginWithEmailAllowed(); @@ -372,7 +332,7 @@ public class RealmAdapter implements RealmModel, JpaModel { if (loginWithEmailAllowed) realm.setDuplicateEmailsAllowed(false); em.flush(); } - + @Override public boolean isDuplicateEmailsAllowed() { return realm.isDuplicateEmailsAllowed(); @@ -1719,6 +1679,16 @@ public class RealmAdapter implements RealmModel, JpaModel { return session.realms().getTopLevelGroups(this); } + @Override + public List getTopLevelGroups(Integer first, Integer max) { + return session.realms().getTopLevelGroups(this, first, max); + } + + @Override + public List 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/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 98b130a8cf..77bc44dc19 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java @@ -17,19 +17,7 @@ package org.keycloak.models.jpa.entities; -import javax.persistence.Access; -import javax.persistence.AccessType; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; -import javax.persistence.Table; +import javax.persistence.*; import java.util.ArrayList; import java.util.Collection; @@ -39,6 +27,8 @@ import java.util.Collection; */ @NamedQueries({ @NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"), + @NamedQuery(name="getGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:search,'%') order by u.name ASC"), + @NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parent is null and u.realm.id = :realm") }) @Entity @Table(name="KEYCLOAK_GROUP") diff --git a/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 16a965f79f..09111cc4bd 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 7431ed2cc0..ab32a22df9 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -17,17 +17,6 @@ package org.keycloak.models.utils; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; @@ -42,60 +31,15 @@ import org.keycloak.credential.CredentialModel; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.AuthDetails; -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.AuthenticationFlowModel; -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientTemplateModel; -import org.keycloak.models.FederatedIdentityModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.IdentityProviderMapperModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelException; -import org.keycloak.models.OTPPolicy; -import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredActionProviderModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserConsentModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; +import org.keycloak.models.*; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.idm.AdminEventRepresentation; -import org.keycloak.representations.idm.AuthDetailsRepresentation; -import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; -import org.keycloak.representations.idm.AuthenticationFlowRepresentation; -import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.ClientTemplateRepresentation; -import org.keycloak.representations.idm.ComponentRepresentation; -import org.keycloak.representations.idm.ConfigPropertyRepresentation; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.representations.idm.EventRepresentation; -import org.keycloak.representations.idm.FederatedIdentityRepresentation; -import org.keycloak.representations.idm.GroupRepresentation; -import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; -import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.representations.idm.ProtocolMapperRepresentation; -import org.keycloak.representations.idm.RealmEventsConfigRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.RequiredActionProviderRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserConsentRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.idm.UserSessionRepresentation; -import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; -import org.keycloak.representations.idm.authorization.PolicyRepresentation; -import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation; -import org.keycloak.representations.idm.authorization.ResourceRepresentation; -import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; -import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.representations.idm.*; +import org.keycloak.representations.idm.authorization.*; import org.keycloak.storage.StorageId; +import java.util.*; +import java.util.stream.Collectors; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -147,10 +91,32 @@ public class ModelToRepresentation { return rep; } + public static List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { + List result = new LinkedList<>(); + List 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 toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) { + List hierarchy = new LinkedList<>(); + List 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 toGroupHierarchy(RealmModel realm, boolean full) { List hierarchy = new LinkedList<>(); List groups = realm.getTopLevelGroups(); - if (groups == null) return hierarchy; + if (Objects.isNull(groups)) return hierarchy; for (GroupModel group : groups) { GroupRepresentation rep = toGroupHierarchy(group, full); hierarchy.add(rep); diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index f6484d6f5b..f90a9d73eb 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 Bill Burke @@ -147,11 +143,11 @@ public interface RealmModel extends RoleContainerModel { boolean isVerifyEmail(); void setVerifyEmail(boolean verifyEmail); - + boolean isLoginWithEmailAllowed(); void setLoginWithEmailAllowed(boolean loginWithEmailAllowed); - + boolean isDuplicateEmailsAllowed(); void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed); @@ -402,6 +398,8 @@ public interface RealmModel extends RoleContainerModel { GroupModel getGroupById(String id); List getGroups(); List getTopLevelGroups(); + List getTopLevelGroups(Integer first, Integer max); + List 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 4e6070f4a6..3e1a1c9221 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -42,6 +42,10 @@ public interface RealmProvider extends Provider { List getTopLevelGroups(RealmModel realm); + List 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); @@ -85,8 +89,6 @@ public interface RealmProvider extends Provider { ClientTemplateModel getClientTemplateById(String id, RealmModel realm); GroupModel getGroupById(String id, RealmModel realm); - - List getRealms(); boolean removeRealm(String id); void close(); 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 5be1c0d958..7c61fe9a3e 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java @@ -26,20 +26,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.services.ErrorResponse; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.List; -import org.keycloak.services.ErrorResponse; +import java.util.Objects; /** * @resource Groups @@ -71,10 +67,22 @@ public class GroupsResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getGroups() { + public List getGroupsByName(@QueryParam("search") String search, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults) { auth.requireView(); - return ModelToRepresentation.toGroupHierarchy(realm, false); + List results; + + if (Objects.nonNull(search)) { + results = ModelToRepresentation.searchForGroupByName(realm, search.trim(), firstResult, maxResults); + } else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) { + results = ModelToRepresentation.toGroupHierarchy(realm, false, firstResult, maxResults); + } else { + results = ModelToRepresentation.toGroupHierarchy(realm, false); + } + + return results; } /** @@ -109,7 +117,7 @@ public class GroupsResource { return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists."); } } - + GroupModel child = null; Response.ResponseBuilder builder = Response.status(204); if (rep.getId() != null) { From f377a45c4e964b1dd244a84b31ad68ff38ae64d4 Mon Sep 17 00:00:00 2001 From: Levente NAGY Date: Wed, 7 Jun 2017 20:52:22 +0200 Subject: [PATCH 2/7] [KEYCLOAK-2538] - groups count for pagination limits --- .../admin/client/resource/GroupsResource.java | 23 +++++++++++++++++++ .../models/cache/infinispan/RealmAdapter.java | 10 ++++++++ .../cache/infinispan/RealmCacheSession.java | 10 ++++++++ .../keycloak/models/jpa/JpaRealmProvider.java | 19 +++++++++++++++ .../org/keycloak/models/jpa/RealmAdapter.java | 10 ++++++++ .../models/jpa/entities/GroupEntity.java | 4 +++- .../java/org/keycloak/models/RealmModel.java | 2 ++ .../org/keycloak/models/RealmProvider.java | 6 +++++ .../resources/admin/GroupsResource.java | 19 +++++++++++++++ 9 files changed, 102 insertions(+), 1 deletion(-) 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 ce6db747fc..2a4b40af24 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 @@ -67,6 +67,29 @@ public interface GroupsResource { @QueryParam("first") Integer first, @QueryParam("max") Integer max); + /** + * Counts all groups. + * @return The number of groups. + */ + @GET + @NoCache + @Path("/count") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Response count(); + + /** + * Counts groups by name search. + * @param search max number of occurrences + * @return The number of group containing search therm. + */ + @GET + @NoCache + @Path("/count") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Response count(@QueryParam("search") String search); + /** * 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 d39646305d..5b99ff1451 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 @@ -1200,6 +1200,16 @@ public class RealmAdapter implements CachedRealmModel { return cacheSession.getGroups(this); } + @Override + public Long getGroupsCount() { + return cacheSession.getGroupsCount(this); + } + + @Override + public Long getGroupsCountByNameContaining(String search) { + return cacheSession.getGroupsCountByNameContaining(this, search); + } + @Override public List getTopLevelGroups() { return cacheSession.getTopLevelGroups(this); 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 47267ada62..6c654238bf 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 @@ -841,6 +841,16 @@ public class RealmCacheSession implements CacheRealmProvider { return list; } + @Override + public Long getGroupsCount(RealmModel realm) { + return getDelegate().getGroupsCount(realm); + } + + @Override + public Long getGroupsCountByNameContaining(RealmModel realm, String search) { + return getDelegate().getGroupsCountByNameContaining(realm, search); + } + @Override public List getTopLevelGroups(RealmModel realm) { String cacheKey = getTopGroupsQueryCacheKey(realm.getId()); 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 683f7a31e6..306f674b7a 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 @@ -322,6 +322,25 @@ public class JpaRealmProvider implements RealmProvider { Collectors.toList(), Collections::unmodifiableList)); } + @Override + public Long getGroupsCount(RealmModel realm) { + Long count = em.createNamedQuery("getGroupCount", Long.class) + .setParameter("realm", realm.getId()) + .getSingleResult(); + + return count; + } + + @Override + public Long getGroupsCountByNameContaining(RealmModel realm, String search) { + Long count = em.createNamedQuery("getGroupCountByNameContaining", Long.class) + .setParameter("realm", realm.getId()) + .setParameter("name", search) + .getSingleResult(); + + return count; + } + @Override public List getTopLevelGroups(RealmModel realm) { RealmEntity ref = em.getReference(RealmEntity.class, realm.getId()); 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 e53ef2f6b6..a217af3a8c 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 @@ -1674,6 +1674,16 @@ public class RealmAdapter implements RealmModel, JpaModel { return session.realms().getGroups(this); } + @Override + public Long getGroupsCount() { + return session.realms().getGroupsCount(this); + } + + @Override + public Long getGroupsCountByNameContaining(String search) { + return session.realms().getGroupsCountByNameContaining(this, search); + } + @Override public List getTopLevelGroups() { return session.realms().getTopLevelGroups(this); 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 77bc44dc19..1f7dc09796 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 @@ -28,7 +28,9 @@ 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="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="getGroupCountByNameContaining", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:name,'%')") }) @Entity @Table(name="KEYCLOAK_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 f90a9d73eb..133c247076 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -397,6 +397,8 @@ public interface RealmModel extends RoleContainerModel { GroupModel getGroupById(String id); List getGroups(); + Long getGroupsCount(); + Long getGroupsCountByNameContaining(String search); List getTopLevelGroups(); List getTopLevelGroups(Integer first, Integer max); List searchForGroupByName(String search, Integer first, Integer max); 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 3e1a1c9221..72c11ad701 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -40,6 +40,10 @@ public interface RealmProvider extends Provider { List getGroups(RealmModel realm); + Long getGroupsCount(RealmModel realm); + + Long getGroupsCountByNameContaining(RealmModel realm, String search); + List getTopLevelGroups(RealmModel realm); List getTopLevelGroups(RealmModel realm, Integer first, Integer max); @@ -89,6 +93,8 @@ public interface RealmProvider extends Provider { ClientTemplateModel getClientTemplateById(String id, RealmModel realm); GroupModel getGroupById(String id, RealmModel realm); + + List getRealms(); boolean removeRealm(String id); void close(); 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 7c61fe9a3e..e5b4676961 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 @@ -101,6 +101,25 @@ public class GroupsResource { return resource; } + /** + * Returns the groups counts. + * + * @return + */ + @GET + @NoCache + @Path("/count") + public Response getGroupCount(@QueryParam("search") String search) { + auth.requireView(); + Long results; + if (Objects.nonNull(search)) { + results = realm.getGroupsCountByNameContaining(search); + } else { + results = realm.getGroupsCount(); + } + return Response.ok(results).build(); + } + /** * 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. From 124bf43a272b3b4fbba6d9d65e52fbab564da2e6 Mon Sep 17 00:00:00 2001 From: Levente NAGY Date: Thu, 22 Jun 2017 17:32:38 +0200 Subject: [PATCH 3/7] [KEYCLOAK-2538] - groups count for pagination --- .../org/keycloak/models/jpa/entities/GroupEntity.java | 2 +- .../services/resources/admin/GroupsResource.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) 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 1f7dc09796..7215df6a6e 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 @@ -30,7 +30,7 @@ import java.util.Collection; @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="getGroupCountByNameContaining", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:name,'%')") + @NamedQuery(name="getGroupCountByNameContaining", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:name,'%')"), }) @Entity @Table(name="KEYCLOAK_GROUP") 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 e5b4676961..1ef8aff7ae 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 @@ -131,13 +131,12 @@ public class GroupsResource { public Response addTopLevelGroup(GroupRepresentation rep) { auth.requireManage(); - for (GroupModel group : realm.getGroups()) { - if (group.getName().equals(rep.getName())) { - return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists."); - } + List 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()); From 2c24b3926853ca08bb41bf0e9a4da82c075b08ff Mon Sep 17 00:00:00 2001 From: Levente NAGY Date: Thu, 7 Sep 2017 19:39:06 +0200 Subject: [PATCH 4/7] KEYCLOAK 2538 - UI group pagination --- .../admin/client/resource/GroupsResource.java | 13 + .../models/cache/infinispan/RealmAdapter.java | 4 +- .../cache/infinispan/RealmCacheSession.java | 4 +- .../keycloak/models/jpa/JpaRealmProvider.java | 11 +- .../org/keycloak/models/jpa/RealmAdapter.java | 4 +- .../models/jpa/entities/GroupEntity.java | 3 +- .../java/org/keycloak/models/RealmModel.java | 2 +- .../org/keycloak/models/RealmProvider.java | 2 +- .../resources/admin/GroupsResource.java | 18 +- .../messages/admin-messages_en.properties | 1 + .../theme/base/admin/resources/js/app.js | 31 ++- .../admin/resources/js/controllers/groups.js | 247 +++++++++++------- .../theme/base/admin/resources/js/loaders.js | 17 +- .../theme/base/admin/resources/js/services.js | 196 +++++++------- .../admin/resources/partials/group-list.html | 71 +++-- 15 files changed, 384 insertions(+), 240 deletions(-) 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 2a4b40af24..e704c399d6 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 @@ -90,6 +90,19 @@ public interface GroupsResource { @Consumes(MediaType.APPLICATION_JSON) Response count(@QueryParam("search") String search); + /** + * Counts groups by name search. + * @param search max number of occurrences + * @param onlyTopGroups true or false for filter only top level groups count + * @return The number of group containing search therm. + */ + @GET + @NoCache + @Path("/count") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + Response count(@QueryParam("search") String search, @QueryParam("top") String 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 af7159cad6..7144c3b474 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 @@ -1213,8 +1213,8 @@ public class RealmAdapter implements CachedRealmModel { } @Override - public Long getGroupsCount() { - return cacheSession.getGroupsCount(this); + public Long getGroupsCount(Boolean onlyTopGroups) { + return cacheSession.getGroupsCount(this, onlyTopGroups); } @Override 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 77f8981ed1..bcff0ba591 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 @@ -843,8 +843,8 @@ public class RealmCacheSession implements CacheRealmProvider { } @Override - public Long getGroupsCount(RealmModel realm) { - return getDelegate().getGroupsCount(realm); + public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) { + return getDelegate().getGroupsCount(realm, onlyTopGroups); } @Override 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 b64a6a6624..2e15fd04af 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,7 @@ package org.keycloak.models.jpa; +import com.sun.org.apache.xpath.internal.operations.Bool; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; import org.keycloak.connections.jpa.util.JpaUtils; @@ -339,8 +340,12 @@ public class JpaRealmProvider implements RealmProvider { } @Override - public Long getGroupsCount(RealmModel realm) { - Long count = em.createNamedQuery("getGroupCount", Long.class) + 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(); @@ -577,7 +582,7 @@ public class JpaRealmProvider implements RealmProvider { @Override public List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { - TypedQuery query = em.createNamedQuery("getGroupIdsByNameContaining", String.class) + TypedQuery query = em.createNamedQuery("getTopLevelGroupIdsByNameContaining", String.class) .setParameter("realm", realm.getId()) .setParameter("search", search); if(Objects.nonNull(first) && Objects.nonNull(max)) { 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 05d7517af5..3b07a73e10 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 @@ -1687,8 +1687,8 @@ public class RealmAdapter implements RealmModel, JpaModel { } @Override - public Long getGroupsCount() { - return session.realms().getGroupsCount(this); + public Long getGroupsCount(Boolean onlyTopGroups) { + return session.realms().getGroupsCount(this, onlyTopGroups); } @Override 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 7215df6a6e..eff0340704 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 @@ -27,9 +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="getTopLevelGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:search,'%') and u.parent is null 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"), @NamedQuery(name="getGroupCountByNameContaining", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:name,'%')"), }) @Entity 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 f8d32e1f64..e88c59ff37 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -400,7 +400,7 @@ public interface RealmModel extends RoleContainerModel { GroupModel getGroupById(String id); List getGroups(); - Long getGroupsCount(); + Long getGroupsCount(Boolean onlyTopGroups); Long getGroupsCountByNameContaining(String search); List getTopLevelGroups(); List getTopLevelGroups(Integer first, Integer max); 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 3b043fc7ad..e1a5d5f7f7 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -40,7 +40,7 @@ public interface RealmProvider extends Provider { List getGroups(RealmModel realm); - Long getGroupsCount(RealmModel realm); + Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups); Long getGroupsCountByNameContaining(RealmModel realm, String search); 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 8d927cfb21..ef704552e9 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; @@ -38,6 +39,8 @@ import java.util.List; 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 @@ -111,15 +114,22 @@ public class GroupsResource { @GET @NoCache @Path("/count") - public Response getGroupCount(@QueryParam("search") String search) { - auth.requireView(); + @Produces(MediaType.APPLICATION_JSON) + public Response getGroupCount(@QueryParam("search") String search, @QueryParam("top") String onlyTopGroups) { Long results; + JSONObject response = new JSONObject(); if (Objects.nonNull(search)) { results = realm.getGroupsCountByNameContaining(search); } else { - results = realm.getGroupsCount(); + results = realm.getGroupsCount(Objects.equals(onlyTopGroups, Boolean.TRUE.toString())); } - return Response.ok(results).build(); + try { + response.put("count", results); + } catch (JSONException e) { + e.printStackTrace(); + return ErrorResponse.error("Cannot create response object", Response.Status.INTERNAL_SERVER_ERROR); + } + return Response.ok(response.toString(), MediaType.APPLICATION_JSON).build(); } /** 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 f261105214..a7601d2f0d 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 @@ -1031,6 +1031,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 c650d00681..2efa53ff52 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 @@ -790,6 +790,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, groups : function(GroupListLoader) { return GroupListLoader(); + }, + groupsCount : function(GroupCountLoader) { + return GroupCountLoader(); } }, controller : 'GroupListCtrl' @@ -1924,7 +1927,7 @@ module.factory('spinnerInterceptor', function($q, $window, $rootScope, $location $('#loading').hide(); } return response; - }, + }, responseError: function(response) { resourceRequests--; if (resourceRequests == 0) { @@ -1944,7 +1947,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(); @@ -2186,17 +2189,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(); @@ -2206,7 +2209,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')) { @@ -2869,35 +2872,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; }; @@ -2927,11 +2930,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 aa0cfadd15..fed4d94078 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,104 @@ -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 ' + i); + }); + 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 ' + i); + }); + var promiseCountChain = promiseCount.promise.then(function(entry) { + groupsCount = entry; + $scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize); + }); + + $q.all([promiseGetGroupsChain, promiseCountChain]); + }; + + $scope.$watch('currentPage', function(newValue, oldValue) { + if(newValue !== oldValue) { + refreshGroups(); + } + }); + + $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 +114,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 +125,7 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group }); }); - } + }; $scope.createGroup = function(selected) { var parent = 'realm'; @@ -61,13 +134,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 +150,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 +168,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 +193,7 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G } - } + }; $scope.cancel = function() { $location.url("/realms/" + realm.realm + "/groups"); }; @@ -176,8 +249,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 +258,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 +283,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 +308,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 +403,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 +417,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 +439,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 +454,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 +472,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 +486,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 044414c478..9b7bab3bda 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 : 1, + 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 fca7b334f1..2fb10dd0ec 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; }); @@ -237,12 +237,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 : ''} @@ -325,9 +325,9 @@ module.factory('RealmSMTPConnectionTester', function($resource) { realm : '@realm', config : '@config' }, { - send: { - method: 'POST' - } + send: { + method: 'POST' + } }); }); @@ -777,67 +777,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 = []; + }); }; @@ -1054,10 +1054,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) { @@ -1075,10 +1075,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', @@ -1142,9 +1142,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); + } } }); @@ -1606,10 +1606,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 efb62bfc9b..7ad6f657b4 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 @@ -
- +
+ - - - - - - - - - + + + + + + + - - -
-
-
- - - - - -
-
-
+
+
+
+
+ +
+ +
+
+
+ +
+
+ + + + + +
+
+
+
+
-
-
- -
+
+ + + + +
+ +
+ \ No newline at end of file From db56d82dbd0cf91f6e0ea7044eb1198309c2b5ca Mon Sep 17 00:00:00 2001 From: Levente NAGY Date: Tue, 12 Sep 2017 11:45:37 +0200 Subject: [PATCH 5/7] KEYCLOAK 2538 - UI group pagination - fix duplicate result for search + sort result --- .../keycloak/models/jpa/JpaRealmProvider.java | 28 ++++++++++++------- .../models/jpa/entities/GroupEntity.java | 5 ++-- .../org/keycloak/models/RealmProvider.java | 2 +- .../admin/resources/js/controllers/groups.js | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) 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 2e15fd04af..17f629fd14 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 @@ -18,6 +18,7 @@ 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; @@ -335,6 +336,7 @@ 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)); } @@ -354,12 +356,7 @@ public class JpaRealmProvider implements RealmProvider { @Override public Long getGroupsCountByNameContaining(RealmModel realm, String search) { - Long count = em.createNamedQuery("getGroupCountByNameContaining", Long.class) - .setParameter("realm", realm.getId()) - .setParameter("name", search) - .getSingleResult(); - - return count; + return (long) searchForGroupByName(realm, search, null, null).size(); } @Override @@ -369,6 +366,7 @@ public class JpaRealmProvider implements RealmProvider { 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)); } @@ -388,6 +386,8 @@ public class JpaRealmProvider implements RealmProvider { } } + list.sort(Comparator.comparing(GroupModel::getName)); + return Collections.unmodifiableList(list); } @@ -582,7 +582,7 @@ public class JpaRealmProvider implements RealmProvider { @Override public List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) { - TypedQuery query = em.createNamedQuery("getTopLevelGroupIdsByNameContaining", String.class) + TypedQuery query = em.createNamedQuery("getGroupIdsByNameContaining", String.class) .setParameter("realm", realm.getId()) .setParameter("search", search); if(Objects.nonNull(first) && Objects.nonNull(max)) { @@ -590,10 +590,18 @@ public class JpaRealmProvider implements RealmProvider { } List groups = query.getResultList(); if (Objects.isNull(groups)) return Collections.EMPTY_LIST; - List list = new LinkedList<>(); + List list = new ArrayList<>(); for (String id : groups) { - list.add(session.realms().getGroupById(id, realm)); + 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); } @@ -645,7 +653,7 @@ public class JpaRealmProvider implements RealmProvider { List 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/entities/GroupEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java index eff0340704..e01ad0c11e 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 @@ -27,11 +27,10 @@ import java.util.Collection; */ @NamedQueries({ @NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"), - @NamedQuery(name="getTopLevelGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:search,'%') and u.parent is null order by u.name ASC"), + @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"), - @NamedQuery(name="getGroupCountByNameContaining", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:name,'%')"), + @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/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java index e1a5d5f7f7..d14f2d6256 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -48,7 +48,7 @@ public interface RealmProvider extends Provider { List getTopLevelGroups(RealmModel realm, Integer first, Integer max); - List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max); + List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max); boolean removeGroup(RealmModel realm, GroupModel group); 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 fed4d94078..23edb675d5 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 @@ -65,7 +65,7 @@ module.controller('GroupListCtrl', function($scope, $route, $q, realm, groups, g $scope.$watch('currentPage', function(newValue, oldValue) { if(newValue !== oldValue) { - refreshGroups(); + refreshGroups($scope.searchTerms); } }); From c8c88dd58c2ec739bf61ec9a45a4087e19cf780f Mon Sep 17 00:00:00 2001 From: Levente NAGY Date: Tue, 12 Sep 2017 15:09:08 +0200 Subject: [PATCH 6/7] KEYCLOAK 2538 - UI group pagination - TU + some code improvement + add mockito dependency --- model/jpa/pom.xml | 5 + .../models/jpa/JpaRealmProviderTest.java | 218 +++++++++++++++++ pom.xml | 7 + .../models/utils/ModelToRepresentation.java | 23 +- services/pom.xml | 5 + .../resources/admin/GroupsResource.java | 1 - .../resources/admin/GroupsResourceTest.java | 230 ++++++++++++++++++ 7 files changed, 469 insertions(+), 20 deletions(-) create mode 100644 model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java create mode 100644 services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml index 54166f5445..5994089985 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -111,6 +111,11 @@ junit test + + org.mockito + mockito-all + test + diff --git a/model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java b/model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java new file mode 100644 index 0000000000..fc8ffa4ae1 --- /dev/null +++ b/model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2016 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.models.jpa; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.jpa.entities.GroupEntity; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.TypedQuery; +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +/** + * @author NAGY Léventé + * @version $Revision: 1 $ + */ +@RunWith(MockitoJUnitRunner.class) +public class JpaRealmProviderTest { + + private JpaRealmProvider subject; + + private RealmModel realmModelMock; + private RealmProvider realmProviderMock; + private KeycloakSession sessionMock; + private EntityManager entityManagerMock; + + @Before + public void setup() { + realmModelMock = mock(RealmModel.class); + realmProviderMock = mock(RealmProvider.class); + sessionMock = mock(KeycloakSession.class); + entityManagerMock = mock(EntityManager.class); + + subject = new JpaRealmProvider(sessionMock, entityManagerMock); + + // Common behaviours + when(realmProviderMock.getGroupById(anyString(), any(RealmModel.class))).thenAnswer((Answer) invocationOnMock -> { + GroupEntity entity = new GroupEntity(); + entity.setId((String) invocationOnMock.getArguments()[0]); + entity.setName((String) invocationOnMock.getArguments()[0]); + return new GroupAdapter(realmModelMock, entityManagerMock, entity); + }); + } + + @Test + public void testGetGroupsCountAllGroups() { + // Given + Long result = 10L; + String idRealm = "idGroup"; + TypedQuery query = mock(TypedQuery.class); + + // When + when(entityManagerMock.createNamedQuery("getGroupCount", Long.class)).thenReturn(query); + when(realmModelMock.getId()).thenReturn(idRealm); + when(query.setParameter("realm", idRealm)).thenReturn(query); + when(query.getSingleResult()).thenReturn(result); + + // Then + Long countResult = subject.getGroupsCount(realmModelMock, false); + + assertEquals(result, countResult); + } + + @Test + public void testGetGroupsCountOnlyTopLevelGroups() { + // Given + Long result = 10L; + String idRealm = "idGroup"; + TypedQuery query = mock(TypedQuery.class); + + // When + when(entityManagerMock.createNamedQuery("getTopLevelGroupCount", Long.class)).thenReturn(query); + when(realmModelMock.getId()).thenReturn(idRealm); + when(query.setParameter("realm", idRealm)).thenReturn(query); + when(query.getSingleResult()).thenReturn(result); + + // Then + Long countResult = subject.getGroupsCount(realmModelMock, true); + + assertEquals(result, countResult); + } + + @Test + public void testSearchForGroupByNameWithAllParams() { + // Given + List result = Arrays.asList("idGroup1", "idGroup2", "idGroup3"); + String idRealm = "idGroup"; + TypedQuery query = mock(TypedQuery.class); + String search = "findMe"; + Integer first = 0; + Integer max = 10; + + // When + when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); + when(realmModelMock.getId()).thenReturn(idRealm); + when(query.setParameter("realm", idRealm)).thenReturn(query); + when(query.setParameter("search", search)).thenReturn(query); + when(query.setFirstResult(first)).thenReturn(query); + when(query.setMaxResults(max)).thenReturn(query); + when(query.getResultList()).thenReturn(result); + when(sessionMock.realms()).thenReturn(realmProviderMock); + + // Then + List searchResult = subject.searchForGroupByName(realmModelMock, search, first, max); + + assertEquals(result.size(), searchResult.size()); + } + + @Test + public void testSearchForGroupByNameWithNullQueryResult() { + // Given + String idRealm = "idGroup"; + TypedQuery query = mock(TypedQuery.class); + String search = "findMe"; + + // When + when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); + when(realmModelMock.getId()).thenReturn(idRealm); + when(query.setParameter("realm", idRealm)).thenReturn(query); + when(query.setParameter("search", search)).thenReturn(query); + when(query.getResultList()).thenReturn(null); + when(sessionMock.realms()).thenReturn(realmProviderMock); + + // Then + List searchResult = subject.searchForGroupByName(realmModelMock, search, null, null); + + assertEquals(Collections.EMPTY_LIST, searchResult); + } + + @Test + public void testSearchForGroupByNameWithNonTopLevelGroupInQueryResult() { + // Given + List result = Arrays.asList("idGroup1", "idGroup2", "idGroup3", "idGroup4"); + String idRealm = "idGroup"; + TypedQuery query = mock(TypedQuery.class); + String search = "findMe"; + Integer first = 0; + Integer max = 10; + + // When + when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); + when(realmModelMock.getId()).thenReturn(idRealm); + when(query.setParameter("realm", idRealm)).thenReturn(query); + when(query.setParameter("search", search)).thenReturn(query); + when(query.setFirstResult(first)).thenReturn(query); + when(query.setMaxResults(max)).thenReturn(query); + when(query.getResultList()).thenReturn(result); + when(sessionMock.realms()).thenReturn(realmProviderMock); + when(realmProviderMock.getGroupById(anyString(), any(RealmModel.class))).thenAnswer((Answer) invocationOnMock -> { + GroupEntity entity = new GroupEntity(); + entity.setId((String) invocationOnMock.getArguments()[0]); + entity.setName((String) invocationOnMock.getArguments()[0]); + if(Arrays.asList("idGroup2", "idGroup4").contains(invocationOnMock.getArguments()[0])) { + entity.setParent(new GroupEntity()); + entity.getParent().setId("idGroup5"); + entity.getParent().setName("idGroup5"); + } + return new GroupAdapter(realmModelMock, entityManagerMock, entity); + }); + + // Then + List searchResult = subject.searchForGroupByName(realmModelMock, search, first, max); + + assertEquals(3,searchResult.size()); + } + + @Test + public void testGetGroupsCountByNameContaining() { + // Given + List result = Arrays.asList("idGroup1", "idGroup2", "idGroup3", "idGroup4"); + String idRealm = "idGroup"; + TypedQuery query = mock(TypedQuery.class); + String search = "findMe"; + + // When + when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); + when(realmModelMock.getId()).thenReturn(idRealm); + when(query.setParameter("realm", idRealm)).thenReturn(query); + when(query.setParameter("search", search)).thenReturn(query); + when(query.getResultList()).thenReturn(result); + when(sessionMock.realms()).thenReturn(realmProviderMock); + + // Then + Long countResult = subject.getGroupsCountByNameContaining(realmModelMock, search); + + verify(query, never()).setFirstResult(anyInt()); + verify(query, never()).setFirstResult(anyInt()); + assertEquals(result.size(), countResult.intValue()); + } +} diff --git a/pom.xml b/pom.xml index 8b216dcbef..e437123938 100755 --- a/pom.xml +++ b/pom.xml @@ -108,6 +108,7 @@ 1.3 2.10 4.12 + 1.9.5 2.7.0.Final 2.35.0 1.4.01 @@ -367,6 +368,12 @@ ${junit.version} test + + org.mockito + mockito-all + test + ${mockito.version} + org.hamcrest hamcrest-all 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 d11c2729db..225117d40e 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 @@ -76,12 +76,7 @@ public class ModelToRepresentation { } else { ClientModel client = (ClientModel)role.getContainer(); String clientId = client.getClientId(); - List currentClientRoles = clientRoleNames.get(clientId); - if (currentClientRoles == null) { - currentClientRoles = new ArrayList<>(); - clientRoleNames.put(clientId, currentClientRoles); - } - + List currentClientRoles = clientRoleNames.computeIfAbsent(clientId, k -> new ArrayList<>()); currentClientRoles.add(role.getName()); } } @@ -153,9 +148,7 @@ public class ModelToRepresentation { List reqActions = new ArrayList(); Set requiredActions = user.getRequiredActions(); - for (String ra : requiredActions){ - reqActions.add(ra); - } + reqActions.addAll(requiredActions); rep.setRequiredActions(reqActions); @@ -609,11 +602,7 @@ public class ModelToRepresentation { Map> grantedProtocolMappers = new HashMap>(); for (ProtocolMapperModel protocolMapper : model.getGrantedProtocolMappers()) { String protocol = protocolMapper.getProtocol(); - List currentProtocolMappers = grantedProtocolMappers.get(protocol); - if (currentProtocolMappers == null) { - currentProtocolMappers = new LinkedList(); - grantedProtocolMappers.put(protocol, currentProtocolMappers); - } + List currentProtocolMappers = grantedProtocolMappers.computeIfAbsent(protocol, k -> new LinkedList()); currentProtocolMappers.add(protocolMapper.getName()); } @@ -626,11 +615,7 @@ public class ModelToRepresentation { ClientModel client2 = (ClientModel) role.getContainer(); String clientId2 = client2.getClientId(); - List currentClientRoles = grantedClientRoles.get(clientId2); - if (currentClientRoles == null) { - currentClientRoles = new LinkedList(); - grantedClientRoles.put(clientId2, currentClientRoles); - } + List currentClientRoles = grantedClientRoles.computeIfAbsent(clientId2, k -> new LinkedList()); currentClientRoles.add(role.getName()); } } diff --git a/services/pom.xml b/services/pom.xml index 733b81277e..cbbf3da206 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -180,6 +180,11 @@ + + org.mockito + mockito-all + test + 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 ef704552e9..958b995786 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 @@ -126,7 +126,6 @@ public class GroupsResource { try { response.put("count", results); } catch (JSONException e) { - e.printStackTrace(); return ErrorResponse.error("Cannot create response object", Response.Status.INTERNAL_SERVER_ERROR); } return Response.ok(response.toString(), MediaType.APPLICATION_JSON).build(); diff --git a/services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java b/services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java new file mode 100644 index 0000000000..9783bbb065 --- /dev/null +++ b/services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2016 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.services.resources.admin; + + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator; +import twitter4j.JSONException; +import twitter4j.JSONObject; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author NAGY Léventé + * @version $Revision: 1 $ + */ +public class GroupsResourceTest { + + private GroupsResource subject; + + private RealmModel realmMock; + private KeycloakSession sessionMock; + private AdminPermissionEvaluator authMock; + private AdminEventBuilder adminEventBuilderMock; + + @Before + public void setup() { + realmMock = mock(RealmModel.class); + sessionMock = mock(KeycloakSession.class); + authMock = mock(AdminPermissionEvaluator.class); + when(authMock.groups()).thenReturn(mock(GroupPermissionEvaluator.class)); + + adminEventBuilderMock = mock(AdminEventBuilder.class); + when(adminEventBuilderMock.resource(ResourceType.GROUP)).thenReturn(adminEventBuilderMock); + + subject= new GroupsResource(realmMock, sessionMock, authMock, adminEventBuilderMock); + } + + @Test + public void testGetGroupWithAllParams() { + // Given + String search = "hello"; + Integer first = 0; + Integer max = 20; + String groupId = "groupId"; + String groupName = "groupName"; + GroupModel groupMock = mock(GroupModel.class); + List groupsList = Collections.singletonList(groupMock); + + // When + when(realmMock.searchForGroupByName(search, first, max)).thenReturn(groupsList); + when(groupMock.getSubGroups()).thenReturn(Collections.EMPTY_SET); + when(groupMock.getId()).thenReturn(groupId); + when(groupMock.getName()).thenReturn(groupName); + when(groupMock.getParent()).thenReturn(null); + + //Then + List result = subject.getGroups(search, first,max); + + Assert.assertEquals(groupsList.size(), result.size()); + Assert.assertEquals(groupId, result.get(0).getId()); + Assert.assertEquals(groupName, result.get(0).getName()); + Assert.assertTrue(result.get(0).getSubGroups().isEmpty()); + } + + @Test + public void testGetGroupWithoutSearch() { + // Given + Integer first = 0; + Integer max = 20; + String groupId = "groupId"; + String groupName = "groupName"; + GroupModel groupMock = mock(GroupModel.class); + List groupsList = Collections.singletonList(groupMock); + + // When + when(realmMock.getTopLevelGroups(first, max)).thenReturn(groupsList); + when(groupMock.getSubGroups()).thenReturn(Collections.EMPTY_SET); + when(groupMock.getId()).thenReturn(groupId); + when(groupMock.getName()).thenReturn(groupName); + when(groupMock.getParent()).thenReturn(null); + + //Then + List result = subject.getGroups(null, first,max); + + Assert.assertEquals(groupsList.size(), result.size()); + Assert.assertEquals(groupId, result.get(0).getId()); + Assert.assertEquals(groupName, result.get(0).getName()); + Assert.assertTrue(result.get(0).getSubGroups().isEmpty()); + } + + @Test + public void testGetGroupWithoutSearchAndPagination() { + // Given + String groupId = "groupId"; + String groupName = "groupName"; + GroupModel groupMock = mock(GroupModel.class); + List groupsList = Collections.singletonList(groupMock); + + // When + when(realmMock.getTopLevelGroups()).thenReturn(groupsList); + when(groupMock.getSubGroups()).thenReturn(Collections.EMPTY_SET); + when(groupMock.getId()).thenReturn(groupId); + when(groupMock.getName()).thenReturn(groupName); + when(groupMock.getParent()).thenReturn(null); + + //Then + List result = subject.getGroups(null, null, null); + + Assert.assertEquals(groupsList.size(), result.size()); + Assert.assertEquals(groupId, result.get(0).getId()); + Assert.assertEquals(groupName, result.get(0).getName()); + Assert.assertTrue(result.get(0).getSubGroups().isEmpty()); + } + + @Test + public void testGetGroupCountWithSearchAndTopLevelFlagTrue() { + // Given + String search = "search"; + Long countResult = 5L; + JSONObject response = new JSONObject(); + try { + response.put("count", countResult); + } catch (JSONException e) { + fail(e.getMessage()); + } + + // When + when(realmMock.getGroupsCountByNameContaining(search)).thenReturn(countResult); + + //Then + Response restResponse = subject.getGroupCount(search, "true"); + + assertEquals(response.toString(), restResponse.getEntity()); + assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); + } + + @Test + public void testGetGroupCountWithoutSearchAndTopLevelFlagTrue() { + // Given + Long countResult = 5L; + JSONObject response = new JSONObject(); + try { + response.put("count", countResult); + } catch (JSONException e) { + fail(e.getMessage()); + } + + // When + when(realmMock.getGroupsCount(true)).thenReturn(countResult); + + //Then + Response restResponse = subject.getGroupCount(null, "true"); + + assertEquals(response.toString(), restResponse.getEntity()); + assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); + } + + @Test + public void testGetGroupCountWithoutSearchAndTopLevelFlagFalse() { + // Given + Long countResult = 5L; + JSONObject response = new JSONObject(); + try { + response.put("count", countResult); + } catch (JSONException e) { + fail(e.getMessage()); + } + + // When + when(realmMock.getGroupsCount(false)).thenReturn(countResult); + + //Then + Response restResponse = subject.getGroupCount(null, "false"); + + assertEquals(response.toString(), restResponse.getEntity()); + assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); + } + + @Test + public void testGetGroupCountWithoutSearchAndTopLevelFlagNull() { + // Given + Long countResult = 5L; + JSONObject response = new JSONObject(); + try { + response.put("count", countResult); + } catch (JSONException e) { + fail(e.getMessage()); + } + + // When + when(realmMock.getGroupsCount(false)).thenReturn(countResult); + + //Then + Response restResponse = subject.getGroupCount(null, null); + + assertEquals(response.toString(), restResponse.getEntity()); + assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); + } +} From e907da77d70afddce3397026746229cc6dcd35e9 Mon Sep 17 00:00:00 2001 From: Levente NAGY Date: Wed, 13 Sep 2017 16:45:45 +0200 Subject: [PATCH 7/7] KEYCLOAK 2538 - UI group pagination - Remove junit mocked TUs, add arquillian Tests, delete mockito from poms, fix groups sorting when get result from cache --- .../admin/client/resource/GroupsResource.java | 20 +- .../cache/infinispan/RealmCacheSession.java | 18 +- model/jpa/pom.xml | 5 - .../models/jpa/JpaRealmProviderTest.java | 218 ----------------- pom.xml | 7 - services/pom.xml | 5 - .../resources/admin/GroupsResource.java | 19 +- .../resources/admin/GroupsResourceTest.java | 230 ------------------ .../testsuite/admin/group/GroupTest.java | 66 ++++- .../admin/resources/js/controllers/groups.js | 6 +- .../theme/base/admin/resources/js/loaders.js | 4 +- 11 files changed, 95 insertions(+), 503 deletions(-) delete mode 100644 model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java delete mode 100644 services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java 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 e704c399d6..6c92204cca 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 @@ -24,6 +24,7 @@ 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 Bill Burke @@ -69,39 +70,38 @@ public interface GroupsResource { /** * Counts all groups. - * @return The number of groups. + * @return A map containing key "count" with number of groups as value. */ @GET @NoCache - @Path("/count") + @Path("count") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - Response count(); + Map count(); /** * Counts groups by name search. * @param search max number of occurrences - * @return The number of group containing search therm. + * @return A map containing key "count" with number of groups as value which matching with search. */ @GET @NoCache - @Path("/count") + @Path("count") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - Response count(@QueryParam("search") String search); + Map count(@QueryParam("search") String search); /** * Counts groups by name search. - * @param search max number of occurrences * @param onlyTopGroups true or false for filter only top level groups count - * @return The number of group containing search therm. + * @return A map containing key "count" with number of top level groups. */ @GET @NoCache - @Path("/count") + @Path("count") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - Response count(@QueryParam("search") String search, @QueryParam("top") String onlyTopGroups); + Map 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 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 bcff0ba591..318bb539c5 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 @@ -94,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; @@ -839,6 +838,9 @@ public class RealmCacheSession implements CacheRealmProvider { } list.add(group); } + + list.sort(Comparator.comparing(GroupModel::getName)); + return list; } @@ -885,6 +887,9 @@ public class RealmCacheSession implements CacheRealmProvider { } list.add(group); } + + list.sort(Comparator.comparing(GroupModel::getName)); + return list; } @@ -921,6 +926,9 @@ public class RealmCacheSession implements CacheRealmProvider { } list.add(group); } + + list.sort(Comparator.comparing(GroupModel::getName)); + return list; } @@ -980,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/pom.xml b/model/jpa/pom.xml index 5994089985..54166f5445 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -111,11 +111,6 @@ junit test - - org.mockito - mockito-all - test - diff --git a/model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java b/model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java deleted file mode 100644 index fc8ffa4ae1..0000000000 --- a/model/jpa/src/test/java/org/keycloak/models/jpa/JpaRealmProviderTest.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2016 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.models.jpa; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RealmProvider; -import org.keycloak.models.jpa.entities.GroupEntity; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.runners.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; - -import javax.persistence.EntityManager; -import javax.persistence.Query; -import javax.persistence.TypedQuery; -import java.util.*; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.*; - -/** - * @author NAGY Léventé - * @version $Revision: 1 $ - */ -@RunWith(MockitoJUnitRunner.class) -public class JpaRealmProviderTest { - - private JpaRealmProvider subject; - - private RealmModel realmModelMock; - private RealmProvider realmProviderMock; - private KeycloakSession sessionMock; - private EntityManager entityManagerMock; - - @Before - public void setup() { - realmModelMock = mock(RealmModel.class); - realmProviderMock = mock(RealmProvider.class); - sessionMock = mock(KeycloakSession.class); - entityManagerMock = mock(EntityManager.class); - - subject = new JpaRealmProvider(sessionMock, entityManagerMock); - - // Common behaviours - when(realmProviderMock.getGroupById(anyString(), any(RealmModel.class))).thenAnswer((Answer) invocationOnMock -> { - GroupEntity entity = new GroupEntity(); - entity.setId((String) invocationOnMock.getArguments()[0]); - entity.setName((String) invocationOnMock.getArguments()[0]); - return new GroupAdapter(realmModelMock, entityManagerMock, entity); - }); - } - - @Test - public void testGetGroupsCountAllGroups() { - // Given - Long result = 10L; - String idRealm = "idGroup"; - TypedQuery query = mock(TypedQuery.class); - - // When - when(entityManagerMock.createNamedQuery("getGroupCount", Long.class)).thenReturn(query); - when(realmModelMock.getId()).thenReturn(idRealm); - when(query.setParameter("realm", idRealm)).thenReturn(query); - when(query.getSingleResult()).thenReturn(result); - - // Then - Long countResult = subject.getGroupsCount(realmModelMock, false); - - assertEquals(result, countResult); - } - - @Test - public void testGetGroupsCountOnlyTopLevelGroups() { - // Given - Long result = 10L; - String idRealm = "idGroup"; - TypedQuery query = mock(TypedQuery.class); - - // When - when(entityManagerMock.createNamedQuery("getTopLevelGroupCount", Long.class)).thenReturn(query); - when(realmModelMock.getId()).thenReturn(idRealm); - when(query.setParameter("realm", idRealm)).thenReturn(query); - when(query.getSingleResult()).thenReturn(result); - - // Then - Long countResult = subject.getGroupsCount(realmModelMock, true); - - assertEquals(result, countResult); - } - - @Test - public void testSearchForGroupByNameWithAllParams() { - // Given - List result = Arrays.asList("idGroup1", "idGroup2", "idGroup3"); - String idRealm = "idGroup"; - TypedQuery query = mock(TypedQuery.class); - String search = "findMe"; - Integer first = 0; - Integer max = 10; - - // When - when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); - when(realmModelMock.getId()).thenReturn(idRealm); - when(query.setParameter("realm", idRealm)).thenReturn(query); - when(query.setParameter("search", search)).thenReturn(query); - when(query.setFirstResult(first)).thenReturn(query); - when(query.setMaxResults(max)).thenReturn(query); - when(query.getResultList()).thenReturn(result); - when(sessionMock.realms()).thenReturn(realmProviderMock); - - // Then - List searchResult = subject.searchForGroupByName(realmModelMock, search, first, max); - - assertEquals(result.size(), searchResult.size()); - } - - @Test - public void testSearchForGroupByNameWithNullQueryResult() { - // Given - String idRealm = "idGroup"; - TypedQuery query = mock(TypedQuery.class); - String search = "findMe"; - - // When - when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); - when(realmModelMock.getId()).thenReturn(idRealm); - when(query.setParameter("realm", idRealm)).thenReturn(query); - when(query.setParameter("search", search)).thenReturn(query); - when(query.getResultList()).thenReturn(null); - when(sessionMock.realms()).thenReturn(realmProviderMock); - - // Then - List searchResult = subject.searchForGroupByName(realmModelMock, search, null, null); - - assertEquals(Collections.EMPTY_LIST, searchResult); - } - - @Test - public void testSearchForGroupByNameWithNonTopLevelGroupInQueryResult() { - // Given - List result = Arrays.asList("idGroup1", "idGroup2", "idGroup3", "idGroup4"); - String idRealm = "idGroup"; - TypedQuery query = mock(TypedQuery.class); - String search = "findMe"; - Integer first = 0; - Integer max = 10; - - // When - when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); - when(realmModelMock.getId()).thenReturn(idRealm); - when(query.setParameter("realm", idRealm)).thenReturn(query); - when(query.setParameter("search", search)).thenReturn(query); - when(query.setFirstResult(first)).thenReturn(query); - when(query.setMaxResults(max)).thenReturn(query); - when(query.getResultList()).thenReturn(result); - when(sessionMock.realms()).thenReturn(realmProviderMock); - when(realmProviderMock.getGroupById(anyString(), any(RealmModel.class))).thenAnswer((Answer) invocationOnMock -> { - GroupEntity entity = new GroupEntity(); - entity.setId((String) invocationOnMock.getArguments()[0]); - entity.setName((String) invocationOnMock.getArguments()[0]); - if(Arrays.asList("idGroup2", "idGroup4").contains(invocationOnMock.getArguments()[0])) { - entity.setParent(new GroupEntity()); - entity.getParent().setId("idGroup5"); - entity.getParent().setName("idGroup5"); - } - return new GroupAdapter(realmModelMock, entityManagerMock, entity); - }); - - // Then - List searchResult = subject.searchForGroupByName(realmModelMock, search, first, max); - - assertEquals(3,searchResult.size()); - } - - @Test - public void testGetGroupsCountByNameContaining() { - // Given - List result = Arrays.asList("idGroup1", "idGroup2", "idGroup3", "idGroup4"); - String idRealm = "idGroup"; - TypedQuery query = mock(TypedQuery.class); - String search = "findMe"; - - // When - when(entityManagerMock.createNamedQuery("getGroupIdsByNameContaining", String.class)).thenReturn(query); - when(realmModelMock.getId()).thenReturn(idRealm); - when(query.setParameter("realm", idRealm)).thenReturn(query); - when(query.setParameter("search", search)).thenReturn(query); - when(query.getResultList()).thenReturn(result); - when(sessionMock.realms()).thenReturn(realmProviderMock); - - // Then - Long countResult = subject.getGroupsCountByNameContaining(realmModelMock, search); - - verify(query, never()).setFirstResult(anyInt()); - verify(query, never()).setFirstResult(anyInt()); - assertEquals(result.size(), countResult.intValue()); - } -} diff --git a/pom.xml b/pom.xml index e437123938..8b216dcbef 100755 --- a/pom.xml +++ b/pom.xml @@ -108,7 +108,6 @@ 1.3 2.10 4.12 - 1.9.5 2.7.0.Final 2.35.0 1.4.01 @@ -368,12 +367,6 @@ ${junit.version} test - - org.mockito - mockito-all - test - ${mockito.version} - org.hamcrest hamcrest-all diff --git a/services/pom.xml b/services/pom.xml index cbbf3da206..733b81277e 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -180,11 +180,6 @@ - - org.mockito - mockito-all - test - 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 958b995786..15be7ae029 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 @@ -35,7 +35,9 @@ 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; @@ -113,22 +115,19 @@ public class GroupsResource { */ @GET @NoCache - @Path("/count") + @Path("count") @Produces(MediaType.APPLICATION_JSON) - public Response getGroupCount(@QueryParam("search") String search, @QueryParam("top") String onlyTopGroups) { + public Map getGroupCount(@QueryParam("search") String search, + @QueryParam("top") @DefaultValue("false") boolean onlyTopGroups) { Long results; - JSONObject response = new JSONObject(); + Map map = new HashMap<>(); if (Objects.nonNull(search)) { results = realm.getGroupsCountByNameContaining(search); } else { - results = realm.getGroupsCount(Objects.equals(onlyTopGroups, Boolean.TRUE.toString())); + results = realm.getGroupsCount(onlyTopGroups); } - try { - response.put("count", results); - } catch (JSONException e) { - return ErrorResponse.error("Cannot create response object", Response.Status.INTERNAL_SERVER_ERROR); - } - return Response.ok(response.toString(), MediaType.APPLICATION_JSON).build(); + map.put("count", results); + return map; } /** diff --git a/services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java b/services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java deleted file mode 100644 index 9783bbb065..0000000000 --- a/services/src/test/java/org/keycloak/services/resources/admin/GroupsResourceTest.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2016 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.services.resources.admin; - - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.events.admin.ResourceType; -import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.representations.idm.GroupRepresentation; -import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; -import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator; -import twitter4j.JSONException; -import twitter4j.JSONObject; - -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author NAGY Léventé - * @version $Revision: 1 $ - */ -public class GroupsResourceTest { - - private GroupsResource subject; - - private RealmModel realmMock; - private KeycloakSession sessionMock; - private AdminPermissionEvaluator authMock; - private AdminEventBuilder adminEventBuilderMock; - - @Before - public void setup() { - realmMock = mock(RealmModel.class); - sessionMock = mock(KeycloakSession.class); - authMock = mock(AdminPermissionEvaluator.class); - when(authMock.groups()).thenReturn(mock(GroupPermissionEvaluator.class)); - - adminEventBuilderMock = mock(AdminEventBuilder.class); - when(adminEventBuilderMock.resource(ResourceType.GROUP)).thenReturn(adminEventBuilderMock); - - subject= new GroupsResource(realmMock, sessionMock, authMock, adminEventBuilderMock); - } - - @Test - public void testGetGroupWithAllParams() { - // Given - String search = "hello"; - Integer first = 0; - Integer max = 20; - String groupId = "groupId"; - String groupName = "groupName"; - GroupModel groupMock = mock(GroupModel.class); - List groupsList = Collections.singletonList(groupMock); - - // When - when(realmMock.searchForGroupByName(search, first, max)).thenReturn(groupsList); - when(groupMock.getSubGroups()).thenReturn(Collections.EMPTY_SET); - when(groupMock.getId()).thenReturn(groupId); - when(groupMock.getName()).thenReturn(groupName); - when(groupMock.getParent()).thenReturn(null); - - //Then - List result = subject.getGroups(search, first,max); - - Assert.assertEquals(groupsList.size(), result.size()); - Assert.assertEquals(groupId, result.get(0).getId()); - Assert.assertEquals(groupName, result.get(0).getName()); - Assert.assertTrue(result.get(0).getSubGroups().isEmpty()); - } - - @Test - public void testGetGroupWithoutSearch() { - // Given - Integer first = 0; - Integer max = 20; - String groupId = "groupId"; - String groupName = "groupName"; - GroupModel groupMock = mock(GroupModel.class); - List groupsList = Collections.singletonList(groupMock); - - // When - when(realmMock.getTopLevelGroups(first, max)).thenReturn(groupsList); - when(groupMock.getSubGroups()).thenReturn(Collections.EMPTY_SET); - when(groupMock.getId()).thenReturn(groupId); - when(groupMock.getName()).thenReturn(groupName); - when(groupMock.getParent()).thenReturn(null); - - //Then - List result = subject.getGroups(null, first,max); - - Assert.assertEquals(groupsList.size(), result.size()); - Assert.assertEquals(groupId, result.get(0).getId()); - Assert.assertEquals(groupName, result.get(0).getName()); - Assert.assertTrue(result.get(0).getSubGroups().isEmpty()); - } - - @Test - public void testGetGroupWithoutSearchAndPagination() { - // Given - String groupId = "groupId"; - String groupName = "groupName"; - GroupModel groupMock = mock(GroupModel.class); - List groupsList = Collections.singletonList(groupMock); - - // When - when(realmMock.getTopLevelGroups()).thenReturn(groupsList); - when(groupMock.getSubGroups()).thenReturn(Collections.EMPTY_SET); - when(groupMock.getId()).thenReturn(groupId); - when(groupMock.getName()).thenReturn(groupName); - when(groupMock.getParent()).thenReturn(null); - - //Then - List result = subject.getGroups(null, null, null); - - Assert.assertEquals(groupsList.size(), result.size()); - Assert.assertEquals(groupId, result.get(0).getId()); - Assert.assertEquals(groupName, result.get(0).getName()); - Assert.assertTrue(result.get(0).getSubGroups().isEmpty()); - } - - @Test - public void testGetGroupCountWithSearchAndTopLevelFlagTrue() { - // Given - String search = "search"; - Long countResult = 5L; - JSONObject response = new JSONObject(); - try { - response.put("count", countResult); - } catch (JSONException e) { - fail(e.getMessage()); - } - - // When - when(realmMock.getGroupsCountByNameContaining(search)).thenReturn(countResult); - - //Then - Response restResponse = subject.getGroupCount(search, "true"); - - assertEquals(response.toString(), restResponse.getEntity()); - assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); - } - - @Test - public void testGetGroupCountWithoutSearchAndTopLevelFlagTrue() { - // Given - Long countResult = 5L; - JSONObject response = new JSONObject(); - try { - response.put("count", countResult); - } catch (JSONException e) { - fail(e.getMessage()); - } - - // When - when(realmMock.getGroupsCount(true)).thenReturn(countResult); - - //Then - Response restResponse = subject.getGroupCount(null, "true"); - - assertEquals(response.toString(), restResponse.getEntity()); - assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); - } - - @Test - public void testGetGroupCountWithoutSearchAndTopLevelFlagFalse() { - // Given - Long countResult = 5L; - JSONObject response = new JSONObject(); - try { - response.put("count", countResult); - } catch (JSONException e) { - fail(e.getMessage()); - } - - // When - when(realmMock.getGroupsCount(false)).thenReturn(countResult); - - //Then - Response restResponse = subject.getGroupCount(null, "false"); - - assertEquals(response.toString(), restResponse.getEntity()); - assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); - } - - @Test - public void testGetGroupCountWithoutSearchAndTopLevelFlagNull() { - // Given - Long countResult = 5L; - JSONObject response = new JSONObject(); - try { - response.put("count", countResult); - } catch (JSONException e) { - fail(e.getMessage()); - } - - // When - when(realmMock.getGroupsCount(false)).thenReturn(countResult); - - //Then - Response restResponse = subject.getGroupCount(null, null); - - assertEquals(response.toString(), restResponse.getEntity()); - assertEquals(MediaType.APPLICATION_JSON, restResponse.getMediaType().toString()); - } -} 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 b7274ecbe3..fd58f7590a 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 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 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 allGroups = realm.groups().groups(); + assertEquals(20, allGroups.size()); + + List slice = realm.groups().groups(5, 7); + assertEquals(7, slice.size()); + + List search = realm.groups().groups("group1",0,20); + assertEquals(11, search.size()); + for(GroupRepresentation group : search) { + assertTrue(group.getName().contains("group1")); + } + + List 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/resources/js/controllers/groups.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js index 23edb675d5..bc3691f9df 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 @@ -36,7 +36,7 @@ module.controller('GroupListCtrl', function($scope, $route, $q, realm, groups, g Groups.query(queryParams, function(entry) { promiseGetGroups.resolve(entry); }, function() { - promiseGetGroups.reject('Unable to fetch ' + i); + promiseGetGroups.reject('Unable to fetch ' + queryParams); }); var promiseGetGroupsChain = promiseGetGroups.promise.then(function(entry) { groups = entry; @@ -53,14 +53,12 @@ module.controller('GroupListCtrl', function($scope, $route, $q, realm, groups, g GroupsCount.query(countParams, function(entry) { promiseCount.resolve(entry); }, function() { - promiseCount.reject('Unable to fetch ' + i); + promiseCount.reject('Unable to fetch ' + countParams); }); var promiseCountChain = promiseCount.promise.then(function(entry) { groupsCount = entry; $scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize); }); - - $q.all([promiseGetGroupsChain, promiseCountChain]); }; $scope.$watch('currentPage', function(newValue, oldValue) { 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 9b7bab3bda..fb49c64f10 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 @@ -491,7 +491,7 @@ module.factory('GroupListLoader', function(Loader, Groups, $route, $q) { return Loader.query(Groups, function() { return { realm : $route.current.params.realm, - first : 1, + first : 0, max : 20 } }); @@ -501,7 +501,7 @@ module.factory('GroupCountLoader', function(Loader, GroupsCount, $route, $q) { return Loader.query(GroupsCount, function() { return { realm : $route.current.params.realm, - top : 'true' + top : true } }); });