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