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) {