Merge pull request #4209 from guitaro/feature/group-search-and-pagination
[KEYCLOAK-2538] - groups pagination and group search
This commit is contained in:
commit
fd025ae76b
18 changed files with 755 additions and 475 deletions
|
@ -20,26 +20,89 @@ package org.keycloak.admin.client.resource;
|
|||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface GroupsResource {
|
||||
|
||||
/**
|
||||
* Get all groups.
|
||||
* @return A list containing all groups.
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<GroupRepresentation> groups();
|
||||
|
||||
/**
|
||||
* Get groups by pagination params.
|
||||
* @param first index of the first element
|
||||
* @param max max number of occurrences
|
||||
* @return A list containing the slice of all groups.
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
List<GroupRepresentation> groups(@QueryParam("first") Integer first, @QueryParam("max") Integer max);
|
||||
|
||||
/**
|
||||
* Get groups by pagination params.
|
||||
* @param search max number of occurrences
|
||||
* @param first index of the first element
|
||||
* @param max max number of occurrences
|
||||
* @return A list containing the slice of all groups.
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
List<GroupRepresentation> groups(@QueryParam("search") String search,
|
||||
@QueryParam("first") Integer first,
|
||||
@QueryParam("max") Integer max);
|
||||
|
||||
/**
|
||||
* Counts all groups.
|
||||
* @return A map containing key "count" with number of groups as value.
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Map<String, Long> count();
|
||||
|
||||
/**
|
||||
* Counts groups by name search.
|
||||
* @param search max number of occurrences
|
||||
* @return A map containing key "count" with number of groups as value which matching with search.
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Map<String, Long> count(@QueryParam("search") String search);
|
||||
|
||||
/**
|
||||
* Counts groups by name search.
|
||||
* @param onlyTopGroups <code>true</code> or <code>false</code> for filter only top level groups count
|
||||
* @return A map containing key "count" with number of top level groups.
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Map<String, Long> count(@QueryParam("top") @DefaultValue("true") boolean onlyTopGroups);
|
||||
|
||||
/**
|
||||
* create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
|
||||
* if the group doesn't exist.
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -1232,11 +1212,31 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
return cacheSession.getGroups(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(Boolean onlyTopGroups) {
|
||||
return cacheSession.getGroupsCount(this, onlyTopGroups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCountByNameContaining(String search) {
|
||||
return cacheSession.getGroupsCountByNameContaining(this, search);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups() {
|
||||
return cacheSession.getTopLevelGroups(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups(Integer first, Integer max) {
|
||||
return cacheSession.getTopLevelGroups(this, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> searchForGroupByName(String search, Integer first, Integer max) {
|
||||
return cacheSession.searchForGroupByName(this, search, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeGroup(GroupModel group) {
|
||||
return cacheSession.removeGroup(this, group);
|
||||
|
|
|
@ -22,48 +22,14 @@ import org.keycloak.cluster.ClusterProvider;
|
|||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
|
||||
import org.keycloak.migration.MigrationModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RealmProvider;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.cache.CacheRealmProvider;
|
||||
import org.keycloak.models.cache.CachedRealmModel;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedClient;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedClientRole;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedClientTemplate;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedGroup;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedRealmRole;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedRole;
|
||||
import org.keycloak.models.cache.infinispan.entities.ClientListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.GroupListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.RealmListQuery;
|
||||
import org.keycloak.models.cache.infinispan.entities.RoleListQuery;
|
||||
import org.keycloak.models.cache.infinispan.events.ClientAddedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.ClientRemovedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.ClientTemplateEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.ClientUpdatedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.GroupAddedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.GroupMovedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.GroupRemovedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.GroupUpdatedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.RoleAddedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.RoleRemovedEvent;
|
||||
import org.keycloak.models.cache.infinispan.events.RoleUpdatedEvent;
|
||||
import org.keycloak.models.cache.infinispan.entities.*;
|
||||
import org.keycloak.models.cache.infinispan.events.*;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -128,7 +94,6 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
protected static final Logger logger = Logger.getLogger(RealmCacheSession.class);
|
||||
public static final String REALM_CLIENTS_QUERY_SUFFIX = ".realm.clients";
|
||||
public static final String ROLES_QUERY_SUFFIX = ".roles";
|
||||
public static final String ROLE_BY_NAME_QUERY_SUFFIX = ".role.by-name";
|
||||
protected RealmCacheManager cache;
|
||||
protected KeycloakSession session;
|
||||
protected RealmProvider delegate;
|
||||
|
@ -873,9 +838,22 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
}
|
||||
list.add(group);
|
||||
}
|
||||
|
||||
list.sort(Comparator.comparing(GroupModel::getName));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
|
||||
return getDelegate().getGroupsCount(realm, onlyTopGroups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
|
||||
return getDelegate().getGroupsCountByNameContaining(realm, search);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups(RealmModel realm) {
|
||||
String cacheKey = getTopGroupsQueryCacheKey(realm.getId());
|
||||
|
@ -909,9 +887,56 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
}
|
||||
list.add(group);
|
||||
}
|
||||
|
||||
list.sort(Comparator.comparing(GroupModel::getName));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups(RealmModel realm, Integer first, Integer max) {
|
||||
String cacheKey = getTopGroupsQueryCacheKey(realm.getId() + first + max);
|
||||
boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId() + first + max);
|
||||
if (queryDB) {
|
||||
return getDelegate().getTopLevelGroups(realm, first, max);
|
||||
}
|
||||
|
||||
GroupListQuery query = cache.get(cacheKey, GroupListQuery.class);
|
||||
if (Objects.nonNull(query)) {
|
||||
logger.tracev("getTopLevelGroups cache hit: {0}", realm.getName());
|
||||
}
|
||||
|
||||
if (Objects.isNull(query)) {
|
||||
Long loaded = cache.getCurrentRevision(cacheKey);
|
||||
List<GroupModel> model = getDelegate().getTopLevelGroups(realm, first, max);
|
||||
if (model == null) return null;
|
||||
Set<String> ids = new HashSet<>();
|
||||
for (GroupModel client : model) ids.add(client.getId());
|
||||
query = new GroupListQuery(loaded, cacheKey, realm, ids);
|
||||
logger.tracev("adding realm getTopLevelGroups cache miss: realm {0} key {1}", realm.getName(), cacheKey);
|
||||
cache.addRevisioned(query, startupRevision);
|
||||
return model;
|
||||
}
|
||||
List<GroupModel> list = new LinkedList<>();
|
||||
for (String id : query.getGroups()) {
|
||||
GroupModel group = session.realms().getGroupById(id, realm);
|
||||
if (Objects.isNull(group)) {
|
||||
invalidations.add(cacheKey);
|
||||
return getDelegate().getTopLevelGroups(realm);
|
||||
}
|
||||
list.add(group);
|
||||
}
|
||||
|
||||
list.sort(Comparator.comparing(GroupModel::getName));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
|
||||
return getDelegate().searchForGroupByName(realm, search, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeGroup(RealmModel realm, GroupModel group) {
|
||||
invalidateGroup(group.getId(), realm.getId(), true);
|
||||
|
@ -963,11 +988,9 @@ public class RealmCacheSession implements CacheRealmProvider {
|
|||
String groupId = eventToAdd.getId();
|
||||
|
||||
// Check if we have existing event with bigger priority
|
||||
boolean eventAlreadyExists = invalidationEvents.stream().filter((InvalidationEvent event) -> {
|
||||
|
||||
return (event.getId().equals(groupId)) && (event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent);
|
||||
|
||||
}).findFirst().isPresent();
|
||||
boolean eventAlreadyExists = invalidationEvents.stream()
|
||||
.anyMatch((InvalidationEvent event) -> (event.getId().equals(groupId)) &&
|
||||
(event instanceof GroupAddedEvent || event instanceof GroupMovedEvent || event instanceof GroupRemovedEvent));
|
||||
|
||||
if (!eventAlreadyExists) {
|
||||
invalidationEvents.add(eventToAdd);
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package org.keycloak.models.jpa;
|
||||
|
||||
import com.sun.org.apache.xpath.internal.operations.Bool;
|
||||
import org.apache.commons.codec.binary.StringUtils;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
|
@ -40,12 +42,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
|||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.TypedQuery;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
@ -343,10 +340,29 @@ public class JpaRealmProvider implements RealmProvider {
|
|||
|
||||
return ref.getGroups().stream()
|
||||
.map(g -> session.realms().getGroupById(g.getId(), realm))
|
||||
.sorted(Comparator.comparing(GroupModel::getName))
|
||||
.collect(Collectors.collectingAndThen(
|
||||
Collectors.toList(), Collections::unmodifiableList));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
|
||||
String query = "getGroupCount";
|
||||
if(Objects.equals(onlyTopGroups, Boolean.TRUE)) {
|
||||
query = "getTopLevelGroupCount";
|
||||
}
|
||||
Long count = em.createNamedQuery(query, Long.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.getSingleResult();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
|
||||
return (long) searchForGroupByName(realm, search, null, null).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups(RealmModel realm) {
|
||||
RealmEntity ref = em.getReference(RealmEntity.class, realm.getId());
|
||||
|
@ -354,10 +370,31 @@ 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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups(RealmModel realm, Integer first, Integer max) {
|
||||
List<String> groupIds = em.createNamedQuery("getTopLevelGroupIds", String.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.setFirstResult(first)
|
||||
.setMaxResults(max)
|
||||
.getResultList();
|
||||
List<GroupModel> list = new ArrayList<>();
|
||||
if(Objects.nonNull(groupIds) && !groupIds.isEmpty()) {
|
||||
for (String id : groupIds) {
|
||||
GroupModel group = getGroupById(id, realm);
|
||||
list.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
list.sort(Comparator.comparing(GroupModel::getName));
|
||||
|
||||
return Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeGroup(RealmModel realm, GroupModel group) {
|
||||
if (group == null) {
|
||||
|
@ -547,6 +584,31 @@ public class JpaRealmProvider implements RealmProvider {
|
|||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
|
||||
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContaining", String.class)
|
||||
.setParameter("realm", realm.getId())
|
||||
.setParameter("search", search);
|
||||
if(Objects.nonNull(first) && Objects.nonNull(max)) {
|
||||
query= query.setFirstResult(first).setMaxResults(max);
|
||||
}
|
||||
List<String> groups = query.getResultList();
|
||||
if (Objects.isNull(groups)) return Collections.EMPTY_LIST;
|
||||
List<GroupModel> list = new ArrayList<>();
|
||||
for (String id : groups) {
|
||||
GroupModel groupById = session.realms().getGroupById(id, realm);
|
||||
while(Objects.nonNull(groupById.getParentId())) {
|
||||
groupById = session.realms().getGroupById(groupById.getParentId(), realm);
|
||||
}
|
||||
if(!list.contains(groupById)) {
|
||||
list.add(groupById);
|
||||
}
|
||||
}
|
||||
list.sort(Comparator.comparing(GroupModel::getName));
|
||||
|
||||
return Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
|
||||
RealmEntity realmEntity = em.find(RealmEntity.class, realm.getId());
|
||||
|
@ -595,7 +657,7 @@ public class JpaRealmProvider implements RealmProvider {
|
|||
List<ClientInitialAccessEntity> entities = query.getResultList();
|
||||
|
||||
return entities.stream()
|
||||
.map(entity -> entityToModel(entity))
|
||||
.map(this::entityToModel)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -1726,11 +1686,31 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
return session.realms().getGroups(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCount(Boolean onlyTopGroups) {
|
||||
return session.realms().getGroupsCount(this, onlyTopGroups);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGroupsCountByNameContaining(String search) {
|
||||
return session.realms().getGroupsCountByNameContaining(this, search);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups() {
|
||||
return session.realms().getTopLevelGroups(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> getTopLevelGroups(Integer first, Integer max) {
|
||||
return session.realms().getTopLevelGroups(this, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GroupModel> searchForGroupByName(String search, Integer first, Integer max) {
|
||||
return session.realms().searchForGroupByName(this, search, first, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeGroup(GroupModel group) {
|
||||
return session.realms().removeGroup(this, group);
|
||||
|
|
|
@ -17,19 +17,7 @@
|
|||
|
||||
package org.keycloak.models.jpa.entities;
|
||||
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.CascadeType;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.NamedQueries;
|
||||
import javax.persistence.NamedQuery;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
|
@ -39,6 +27,10 @@ import java.util.Collection;
|
|||
*/
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"),
|
||||
@NamedQuery(name="getGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm.id = :realm and u.name like concat('%',:search,'%') order by u.name ASC"),
|
||||
@NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parent is null and u.realm.id = :realm"),
|
||||
@NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm.id = :realm"),
|
||||
@NamedQuery(name="getTopLevelGroupCount", query="select count(u) from GroupEntity u where u.realm.id = :realm and u.parent is null")
|
||||
})
|
||||
@Entity
|
||||
@Table(name="KEYCLOAK_GROUP")
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -17,17 +17,6 @@
|
|||
|
||||
package org.keycloak.models.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.model.Resource;
|
||||
|
@ -42,60 +31,15 @@ import org.keycloak.credential.CredentialModel;
|
|||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.admin.AdminEvent;
|
||||
import org.keycloak.events.admin.AuthDetails;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientTemplateModel;
|
||||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserConsentModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||
import org.keycloak.representations.idm.AuthDetailsRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientTemplateRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserConsentRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||
import org.keycloak.representations.idm.*;
|
||||
import org.keycloak.representations.idm.authorization.*;
|
||||
import org.keycloak.storage.StorageId;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
|
@ -131,12 +75,7 @@ public class ModelToRepresentation {
|
|||
} else {
|
||||
ClientModel client = (ClientModel)role.getContainer();
|
||||
String clientId = client.getClientId();
|
||||
List<String> currentClientRoles = clientRoleNames.get(clientId);
|
||||
if (currentClientRoles == null) {
|
||||
currentClientRoles = new ArrayList<>();
|
||||
clientRoleNames.put(clientId, currentClientRoles);
|
||||
}
|
||||
|
||||
List<String> currentClientRoles = clientRoleNames.computeIfAbsent(clientId, k -> new ArrayList<>());
|
||||
currentClientRoles.add(role.getName());
|
||||
}
|
||||
}
|
||||
|
@ -147,10 +86,32 @@ public class ModelToRepresentation {
|
|||
return rep;
|
||||
}
|
||||
|
||||
public static List<GroupRepresentation> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
|
||||
List<GroupRepresentation> result = new LinkedList<>();
|
||||
List<GroupModel> groups = realm.searchForGroupByName(search, first, max);
|
||||
if (Objects.isNull(groups)) return result;
|
||||
for (GroupModel group : groups) {
|
||||
GroupRepresentation rep = toGroupHierarchy(group, false);
|
||||
result.add(rep);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) {
|
||||
List<GroupRepresentation> hierarchy = new LinkedList<>();
|
||||
List<GroupModel> groups = realm.getTopLevelGroups(first, max);
|
||||
if (Objects.isNull(groups)) return hierarchy;
|
||||
for (GroupModel group : groups) {
|
||||
GroupRepresentation rep = toGroupHierarchy(group, full);
|
||||
hierarchy.add(rep);
|
||||
}
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
public static List<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full) {
|
||||
List<GroupRepresentation> hierarchy = new LinkedList<>();
|
||||
List<GroupModel> groups = realm.getTopLevelGroups();
|
||||
if (groups == null) return hierarchy;
|
||||
if (Objects.isNull(groups)) return hierarchy;
|
||||
for (GroupModel group : groups) {
|
||||
GroupRepresentation rep = toGroupHierarchy(group, full);
|
||||
hierarchy.add(rep);
|
||||
|
@ -188,9 +149,7 @@ public class ModelToRepresentation {
|
|||
|
||||
List<String> reqActions = new ArrayList<String>();
|
||||
Set<String> requiredActions = user.getRequiredActions();
|
||||
for (String ra : requiredActions){
|
||||
reqActions.add(ra);
|
||||
}
|
||||
reqActions.addAll(requiredActions);
|
||||
|
||||
rep.setRequiredActions(reqActions);
|
||||
|
||||
|
@ -644,11 +603,7 @@ public class ModelToRepresentation {
|
|||
Map<String, List<String>> grantedProtocolMappers = new HashMap<String, List<String>>();
|
||||
for (ProtocolMapperModel protocolMapper : model.getGrantedProtocolMappers()) {
|
||||
String protocol = protocolMapper.getProtocol();
|
||||
List<String> currentProtocolMappers = grantedProtocolMappers.get(protocol);
|
||||
if (currentProtocolMappers == null) {
|
||||
currentProtocolMappers = new LinkedList<String>();
|
||||
grantedProtocolMappers.put(protocol, currentProtocolMappers);
|
||||
}
|
||||
List<String> currentProtocolMappers = grantedProtocolMappers.computeIfAbsent(protocol, k -> new LinkedList<String>());
|
||||
currentProtocolMappers.add(protocolMapper.getName());
|
||||
}
|
||||
|
||||
|
@ -661,11 +616,7 @@ public class ModelToRepresentation {
|
|||
ClientModel client2 = (ClientModel) role.getContainer();
|
||||
|
||||
String clientId2 = client2.getClientId();
|
||||
List<String> currentClientRoles = grantedClientRoles.get(clientId2);
|
||||
if (currentClientRoles == null) {
|
||||
currentClientRoles = new LinkedList<String>();
|
||||
grantedClientRoles.put(clientId2, currentClientRoles);
|
||||
}
|
||||
List<String> currentClientRoles = grantedClientRoles.computeIfAbsent(clientId2, k -> new LinkedList<String>());
|
||||
currentClientRoles.add(role.getName());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,7 @@ import org.keycloak.provider.ProviderEvent;
|
|||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -404,7 +400,11 @@ public interface RealmModel extends RoleContainerModel {
|
|||
|
||||
GroupModel getGroupById(String id);
|
||||
List<GroupModel> getGroups();
|
||||
Long getGroupsCount(Boolean onlyTopGroups);
|
||||
Long getGroupsCountByNameContaining(String search);
|
||||
List<GroupModel> getTopLevelGroups();
|
||||
List<GroupModel> getTopLevelGroups(Integer first, Integer max);
|
||||
List<GroupModel> searchForGroupByName(String search, Integer first, Integer max);
|
||||
boolean removeGroup(GroupModel group);
|
||||
void moveGroup(GroupModel group, GroupModel toParent);
|
||||
|
||||
|
|
|
@ -40,8 +40,16 @@ public interface RealmProvider extends Provider {
|
|||
|
||||
List<GroupModel> getGroups(RealmModel realm);
|
||||
|
||||
Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups);
|
||||
|
||||
Long getGroupsCountByNameContaining(RealmModel realm, String search);
|
||||
|
||||
List<GroupModel> getTopLevelGroups(RealmModel realm);
|
||||
|
||||
List<GroupModel> getTopLevelGroups(RealmModel realm, Integer first, Integer max);
|
||||
|
||||
List searchForGroupByName(RealmModel realm, String search, Integer first, Integer max);
|
||||
|
||||
boolean removeGroup(RealmModel realm, GroupModel group);
|
||||
|
||||
GroupModel createGroup(RealmModel realm, String name);
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
|
@ -26,21 +27,22 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import twitter4j.JSONException;
|
||||
import twitter4j.JSONObject;
|
||||
|
||||
/**
|
||||
* @resource Groups
|
||||
|
@ -71,10 +73,22 @@ public class GroupsResource {
|
|||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<GroupRepresentation> getGroups() {
|
||||
public List<GroupRepresentation> getGroups(@QueryParam("search") String search,
|
||||
@QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults) {
|
||||
auth.groups().requireList();
|
||||
|
||||
return ModelToRepresentation.toGroupHierarchy(realm, false);
|
||||
List<GroupRepresentation> results;
|
||||
|
||||
if (Objects.nonNull(search)) {
|
||||
results = ModelToRepresentation.searchForGroupByName(realm, search.trim(), firstResult, maxResults);
|
||||
} else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) {
|
||||
results = ModelToRepresentation.toGroupHierarchy(realm, false, firstResult, maxResults);
|
||||
} else {
|
||||
results = ModelToRepresentation.toGroupHierarchy(realm, false);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,6 +108,28 @@ public class GroupsResource {
|
|||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the groups counts.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@NoCache
|
||||
@Path("count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Map<String, Long> getGroupCount(@QueryParam("search") String search,
|
||||
@QueryParam("top") @DefaultValue("false") boolean onlyTopGroups) {
|
||||
Long results;
|
||||
Map<String, Long> map = new HashMap<>();
|
||||
if (Objects.nonNull(search)) {
|
||||
results = realm.getGroupsCountByNameContaining(search);
|
||||
} else {
|
||||
results = realm.getGroupsCount(onlyTopGroups);
|
||||
}
|
||||
map.put("count", results);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
|
||||
* if the group doesn't exist.
|
||||
|
@ -105,13 +141,12 @@ public class GroupsResource {
|
|||
public Response addTopLevelGroup(GroupRepresentation rep) {
|
||||
auth.groups().requireManage();
|
||||
|
||||
for (GroupModel group : realm.getGroups()) {
|
||||
if (group.getName().equals(rep.getName())) {
|
||||
List<GroupRepresentation> search = ModelToRepresentation.searchForGroupByName(realm, rep.getName(), 0, 1);
|
||||
if (search != null && !search.isEmpty() && Objects.equals(search.get(0).getName(), rep.getName())) {
|
||||
return ErrorResponse.exists("Top level group named '" + rep.getName() + "' already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
GroupModel child = null;
|
||||
GroupModel child;
|
||||
Response.ResponseBuilder builder = Response.status(204);
|
||||
if (rep.getId() != null) {
|
||||
child = realm.getGroupById(rep.getId());
|
||||
|
|
|
@ -92,7 +92,6 @@ public class GroupTest extends AbstractGroupTest {
|
|||
user.setCredentials(credentials);
|
||||
users.add(user);
|
||||
|
||||
|
||||
List<ClientRepresentation> clients = testRealmRep.getClients();
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
|
@ -634,4 +633,59 @@ public class GroupTest extends AbstractGroupTest {
|
|||
assertEquals(110, group.members(0, 1000).size());
|
||||
assertEquals(110, group.members(-1, -2).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchAndCountGroups() throws Exception {
|
||||
String firstGroupId = "";
|
||||
|
||||
RealmResource realm = adminClient.realms().realm("test");
|
||||
|
||||
// Clean up all test groups
|
||||
for (GroupRepresentation group : realm.groups().groups()) {
|
||||
GroupResource resource = realm.groups().group(group.getId());
|
||||
resource.remove();
|
||||
assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.groupPath(group.getId()), ResourceType.GROUP);
|
||||
}
|
||||
|
||||
// Add 20 new groups with known names
|
||||
for (int i=0;i<20;i++) {
|
||||
GroupRepresentation group = new GroupRepresentation();
|
||||
group.setName("group"+i);
|
||||
group = createGroup(realm, group);
|
||||
if(i== 0) {
|
||||
firstGroupId = group.getId();
|
||||
}
|
||||
}
|
||||
|
||||
// Get groups by search and pagination
|
||||
List<GroupRepresentation> allGroups = realm.groups().groups();
|
||||
assertEquals(20, allGroups.size());
|
||||
|
||||
List<GroupRepresentation> slice = realm.groups().groups(5, 7);
|
||||
assertEquals(7, slice.size());
|
||||
|
||||
List<GroupRepresentation> search = realm.groups().groups("group1",0,20);
|
||||
assertEquals(11, search.size());
|
||||
for(GroupRepresentation group : search) {
|
||||
assertTrue(group.getName().contains("group1"));
|
||||
}
|
||||
|
||||
List<GroupRepresentation> noResultSearch = realm.groups().groups("abcd",0,20);
|
||||
assertEquals(0, noResultSearch.size());
|
||||
|
||||
// Count
|
||||
assertEquals(new Long(allGroups.size()), realm.groups().count().get("count"));
|
||||
assertEquals(new Long(search.size()), realm.groups().count("group1").get("count"));
|
||||
assertEquals(new Long(noResultSearch.size()), realm.groups().count("abcd").get("count"));
|
||||
|
||||
// Add a subgroup for onlyTopLevel flag testing
|
||||
GroupRepresentation level2Group = new GroupRepresentation();
|
||||
level2Group.setName("group1111");
|
||||
Response response = realm.groups().group(firstGroupId).subGroup(level2Group);
|
||||
response.close();
|
||||
assertAdminEvents.assertEvent("test", OperationType.CREATE, AdminEventPaths.groupSubgroupsPath(firstGroupId), level2Group, ResourceType.GROUP);
|
||||
|
||||
assertEquals(new Long(allGroups.size()), realm.groups().count(true).get("count"));
|
||||
assertEquals(new Long(allGroups.size() + 1), realm.groups().count(false).get("count"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1036,6 +1036,7 @@ group-membership.tooltip=Groups user is a member of. Select a listed group and c
|
|||
membership.available-groups.tooltip=Groups a user can join. Select a group and click the join button.
|
||||
table-of-realm-users=Table of Realm Users
|
||||
view-all-users=View all users
|
||||
view-all-groups=View all groups
|
||||
unlock-users=Unlock users
|
||||
no-users-available=No users available
|
||||
users.instruction=Please enter a search, or click on view all users
|
||||
|
|
|
@ -802,6 +802,9 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
groups : function(GroupListLoader) {
|
||||
return GroupListLoader();
|
||||
},
|
||||
groupsCount : function(GroupCountLoader) {
|
||||
return GroupCountLoader();
|
||||
}
|
||||
},
|
||||
controller : 'GroupListCtrl'
|
||||
|
|
|
@ -1,31 +1,102 @@
|
|||
module.controller('GroupListCtrl', function($scope, $route, realm, groups, Groups, Group, GroupChildren, Notifications, $location, Dialog) {
|
||||
module.controller('GroupListCtrl', function($scope, $route, $q, realm, groups, groupsCount, Groups, GroupsCount, Group, GroupChildren, Notifications, $location, Dialog) {
|
||||
$scope.realm = realm;
|
||||
$scope.groupList = [
|
||||
{"id" : "realm", "name": "Groups",
|
||||
"subGroups" : groups}
|
||||
{
|
||||
"id" : "realm",
|
||||
"name": "Groups",
|
||||
"subGroups" : groups
|
||||
}
|
||||
];
|
||||
|
||||
$scope.searchTerms = '';
|
||||
$scope.currentPage = 1;
|
||||
$scope.currentPageInput = $scope.currentPage;
|
||||
$scope.pageSize = groups.length;
|
||||
$scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize);
|
||||
|
||||
$scope.tree = [];
|
||||
|
||||
$scope.edit = function(selected) {
|
||||
if (selected.id == 'realm') return;
|
||||
$location.url("/realms/" + realm.realm + "/groups/" + selected.id);
|
||||
var refreshGroups = function (search) {
|
||||
var queryParams = {
|
||||
realm : realm.id,
|
||||
first : ($scope.currentPage * $scope.pageSize) - $scope.pageSize,
|
||||
max : $scope.pageSize
|
||||
};
|
||||
var countParams = {
|
||||
realm : realm.id,
|
||||
top : 'true'
|
||||
};
|
||||
|
||||
if(angular.isDefined(search) && search !== '') {
|
||||
queryParams.search = search;
|
||||
countParams.search = search;
|
||||
}
|
||||
|
||||
var promiseGetGroups = $q.defer();
|
||||
Groups.query(queryParams, function(entry) {
|
||||
promiseGetGroups.resolve(entry);
|
||||
}, function() {
|
||||
promiseGetGroups.reject('Unable to fetch ' + queryParams);
|
||||
});
|
||||
var promiseGetGroupsChain = promiseGetGroups.promise.then(function(entry) {
|
||||
groups = entry;
|
||||
$scope.groupList = [
|
||||
{
|
||||
"id" : "realm",
|
||||
"name": "Groups",
|
||||
"subGroups" : groups
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
var promiseCount = $q.defer();
|
||||
GroupsCount.query(countParams, function(entry) {
|
||||
promiseCount.resolve(entry);
|
||||
}, function() {
|
||||
promiseCount.reject('Unable to fetch ' + countParams);
|
||||
});
|
||||
var promiseCountChain = promiseCount.promise.then(function(entry) {
|
||||
groupsCount = entry;
|
||||
$scope.numberOfPages = Math.ceil(groupsCount.count/$scope.pageSize);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('currentPage', function(newValue, oldValue) {
|
||||
if(newValue !== oldValue) {
|
||||
refreshGroups($scope.searchTerms);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.clearSearch = function() {
|
||||
$scope.searchTerms = '';
|
||||
$scope.currentPage = 1;
|
||||
refreshGroups();
|
||||
};
|
||||
|
||||
$scope.searchGroup = function() {
|
||||
$scope.currentPage = 1;
|
||||
refreshGroups($scope.searchTerms);
|
||||
};
|
||||
|
||||
$scope.edit = function(selected) {
|
||||
if (selected.id === 'realm') return;
|
||||
$location.url("/realms/" + realm.realm + "/groups/" + selected.id);
|
||||
};
|
||||
|
||||
$scope.cut = function(selected) {
|
||||
$scope.cutNode = selected;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isDisabled = function() {
|
||||
if (!$scope.tree.currentNode) return true;
|
||||
return $scope.tree.currentNode.id == 'realm';
|
||||
}
|
||||
return $scope.tree.currentNode.id === 'realm';
|
||||
};
|
||||
|
||||
$scope.paste = function(selected) {
|
||||
if (selected == null) return;
|
||||
if ($scope.cutNode == null) return;
|
||||
if (selected.id == $scope.cutNode.id) return;
|
||||
if (selected.id == 'realm') {
|
||||
if (selected === null) return;
|
||||
if ($scope.cutNode === null) return;
|
||||
if (selected.id === $scope.cutNode.id) return;
|
||||
if (selected.id === 'realm') {
|
||||
Groups.save({realm: realm.realm}, {id:$scope.cutNode.id}, function() {
|
||||
$route.reload();
|
||||
Notifications.success("Group moved.");
|
||||
|
@ -41,10 +112,10 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$scope.remove = function(selected) {
|
||||
if (selected == null) return;
|
||||
if (selected === null) return;
|
||||
Dialog.confirmDelete(selected.name, 'group', function() {
|
||||
Group.remove({ realm: realm.realm, groupId : selected.id }, function() {
|
||||
$route.reload();
|
||||
|
@ -52,7 +123,7 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
|
|||
});
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$scope.createGroup = function(selected) {
|
||||
var parent = 'realm';
|
||||
|
@ -61,13 +132,13 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
|
|||
}
|
||||
$location.url("/create/group/" + realm.realm + '/parent/' + parent);
|
||||
|
||||
}
|
||||
};
|
||||
var isLeaf = function(node) {
|
||||
return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
|
||||
}
|
||||
return node.id !== "realm" && (!node.subGroups || node.subGroups.length === 0);
|
||||
};
|
||||
|
||||
$scope.getGroupClass = function(node) {
|
||||
if (node.id == "realm") {
|
||||
if (node.id === "realm") {
|
||||
return 'pficon pficon-users';
|
||||
}
|
||||
if (isLeaf(node)) {
|
||||
|
@ -77,12 +148,12 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
|
|||
if (node.subGroups.length && !node.collapsed) return 'expanded';
|
||||
return 'collapsed';
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getSelectedClass = function(node) {
|
||||
if (node.selected) {
|
||||
return 'selected';
|
||||
} else if ($scope.cutNode && $scope.cutNode.id == node.id) {
|
||||
} else if ($scope.cutNode && $scope.cutNode.id === node.id) {
|
||||
return 'cut';
|
||||
}
|
||||
return undefined;
|
||||
|
@ -95,8 +166,8 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
|
|||
$scope.group = {};
|
||||
$scope.save = function() {
|
||||
console.log('save!!!');
|
||||
if (parentId == 'realm') {
|
||||
console.log('realm')
|
||||
if (parentId === 'realm') {
|
||||
console.log('realm');
|
||||
Groups.save({realm: realm.realm}, $scope.group, function(data, headers) {
|
||||
var l = headers().location;
|
||||
|
||||
|
@ -120,7 +191,7 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
$scope.cancel = function() {
|
||||
$location.url("/realms/" + realm.realm + "/groups");
|
||||
};
|
||||
|
@ -176,8 +247,7 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou
|
|||
var attrs = $scope.group.attributes;
|
||||
for (var attribute in attrs) {
|
||||
if (typeof attrs[attribute] === "string") {
|
||||
var attrVals = attrs[attribute].split("##");
|
||||
attrs[attribute] = attrVals;
|
||||
attrs[attribute] = attrs[attribute].split("##");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -186,8 +256,7 @@ module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Grou
|
|||
var attrs = group.attributes;
|
||||
for (var attribute in attrs) {
|
||||
if (typeof attrs[attribute] === "object") {
|
||||
var attrVals = attrs[attribute].join("##");
|
||||
attrs[attribute] = attrVals;
|
||||
attrs[attribute] = attrs[attribute].join("##");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -332,13 +401,13 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
|
|||
groupId: group.id,
|
||||
max : 5,
|
||||
first : 0
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$scope.firstPage = function() {
|
||||
$scope.query.first = 0;
|
||||
$scope.searchQuery();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.previousPage = function() {
|
||||
$scope.query.first -= parseInt($scope.query.max);
|
||||
|
@ -346,12 +415,12 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
|
|||
$scope.query.first = 0;
|
||||
}
|
||||
$scope.searchQuery();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.nextPage = function() {
|
||||
$scope.query.first += parseInt($scope.query.max);
|
||||
$scope.searchQuery();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.searchQuery = function() {
|
||||
console.log("query.search: " + $scope.query.search);
|
||||
|
@ -368,7 +437,7 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
|
|||
|
||||
});
|
||||
|
||||
module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications, $location, Dialog) {
|
||||
module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications) {
|
||||
$scope.realm = realm;
|
||||
$scope.groupList = groups;
|
||||
$scope.selectedGroup = null;
|
||||
|
@ -383,7 +452,7 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
|
|||
if (!$scope.tree.currentNode) {
|
||||
Notifications.error('Please select a group to add');
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
DefaultGroups.update({realm: realm.realm, groupId: $scope.tree.currentNode.id}, function() {
|
||||
Notifications.success('Added default group');
|
||||
|
@ -401,11 +470,11 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
|
|||
};
|
||||
|
||||
var isLeaf = function(node) {
|
||||
return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
|
||||
return node.id !== "realm" && (!node.subGroups || node.subGroups.length === 0);
|
||||
};
|
||||
|
||||
$scope.getGroupClass = function(node) {
|
||||
if (node.id == "realm") {
|
||||
if (node.id === "realm") {
|
||||
return 'pficon pficon-users';
|
||||
}
|
||||
if (isLeaf(node)) {
|
||||
|
@ -415,12 +484,12 @@ module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, D
|
|||
if (node.subGroups.length && !node.collapsed) return 'expanded';
|
||||
return 'collapsed';
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getSelectedClass = function(node) {
|
||||
if (node.selected) {
|
||||
return 'selected';
|
||||
} else if ($scope.cutNode && $scope.cutNode.id == node.id) {
|
||||
} else if ($scope.cutNode && $scope.cutNode.id === node.id) {
|
||||
return 'cut';
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
@ -15,7 +15,7 @@ module.factory('Loader', function($q) {
|
|||
});
|
||||
return delay.promise;
|
||||
};
|
||||
}
|
||||
};
|
||||
loader.query = function(service, id) {
|
||||
return function() {
|
||||
var i = id && id();
|
||||
|
@ -27,7 +27,7 @@ module.factory('Loader', function($q) {
|
|||
});
|
||||
return delay.promise;
|
||||
};
|
||||
}
|
||||
};
|
||||
return loader;
|
||||
});
|
||||
|
||||
|
@ -490,7 +490,18 @@ module.factory('AuthenticationConfigLoader', function(Loader, AuthenticationConf
|
|||
module.factory('GroupListLoader', function(Loader, Groups, $route, $q) {
|
||||
return Loader.query(Groups, function() {
|
||||
return {
|
||||
realm : $route.current.params.realm
|
||||
realm : $route.current.params.realm,
|
||||
first : 0,
|
||||
max : 20
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('GroupCountLoader', function(Loader, GroupsCount, $route, $q) {
|
||||
return Loader.query(GroupsCount, function() {
|
||||
return {
|
||||
realm : $route.current.params.realm,
|
||||
top : true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1625,10 +1625,26 @@ module.factory('GroupChildren', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('GroupsCount', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/groups/count', {
|
||||
realm : '@realm'
|
||||
},
|
||||
{
|
||||
query: {
|
||||
isArray: false,
|
||||
method: 'GET',
|
||||
params: {},
|
||||
transformResponse: function (data) {
|
||||
return angular.fromJson(data)
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('Groups', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/groups', {
|
||||
realm : '@realm'
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
module.factory('GroupRealmRoleMapping', function($resource) {
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<kc-tabs-group-list></kc-tabs-group-list>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<table class="table table-striped table-bordered" style="margin-bottom: 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kc-table-actions" colspan="5">
|
||||
<div class="form-inline">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchTerms" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-search" id="groupSearch" ng-click="searchGroup()"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
|
||||
<div class="pull-right" data-ng-show="access.manageUsers">
|
||||
<div class="form-inline">
|
||||
<button id="createGroup" class="btn btn-default" ng-click="createGroup(tree.currentNode)">{{:: 'new' | translate}}</button>
|
||||
<button id="editGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="edit(tree.currentNode)">{{:: 'edit' | translate}}</button>
|
||||
<button id="cutGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="cut(tree.currentNode)">{{:: 'cut' | translate}}</button>
|
||||
|
@ -14,13 +24,14 @@
|
|||
<button id="removeGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="remove(tree.currentNode)">{{:: 'delete' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <div
|
||||
tree-id="tree"
|
||||
<td>
|
||||
<div tree-id="tree"
|
||||
angular-treeview="true"
|
||||
tree-model="groupList"
|
||||
node-id="id"
|
||||
|
@ -31,7 +42,9 @@
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-bottom: 50px">
|
||||
<kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
Loading…
Reference in a new issue