KEYCLOAK-8613 Group Membership Pagination

This commit is contained in:
Leon Graser 2018-10-05 14:25:55 +00:00 committed by Marek Posolda
parent 39bf08e1b9
commit 85f11873c3
19 changed files with 922 additions and 180 deletions

View file

@ -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);

View file

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

View file

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

View file

@ -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> {
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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());
}
}

View file

@ -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());
});
}

View file

@ -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'

View file

@ -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();
});
};

View file

@ -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;

View file

@ -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)
}
}
});
});

View file

@ -1,80 +1,91 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<kc-tabs-group-list></kc-tabs-group-list>
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<kc-tabs-group-list></kc-tabs-group-list>
<form class="form-horizontal" name="realmForm" novalidate>
<div class="form-group" kc-read-only="!access.manageRealm">
<label class="col-md-1 control-label" class="control-label"></label>
<form class="form-horizontal" name="realmForm" novalidate>
<div class="form-group" kc-read-only="!access.manageRealm">
<label class="col-md-1 control-label" class="control-label"></label>
<div class="col-md-8" >
<div class="row">
<div class="col-md-5">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<label class="control-label">{{:: 'default-groups' | translate}}</label>
<kc-tooltip>{{:: 'default-groups.tooltip' | translate}}</kc-tooltip>
<div class="col-md-8" >
<div class="row">
<div class="col-md-5">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<label class="control-label">{{:: 'default-groups' | translate}}</label>
<kc-tooltip>{{:: 'default-groups.tooltip' | translate}}</kc-tooltip>
<div class="pull-right" data-ng-show="access.manageRealm">
<button id="removeDefaultGroup" class="btn btn-default" ng-click="removeDefaultGroup()">{{:: 'remove' | translate}}</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select id="defaultGroups" class="form-control" size=5
ng-model="selectedGroup"
ng-options="r.path for r in defaultGroups">
<option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
</select>
<div class="pull-right" data-ng-show="access.manageRealm">
<button id="removeDefaultGroup" class="btn btn-default" ng-click="removeDefaultGroup()">{{:: 'remove' | translate}}</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<select id="defaultGroups" class="form-control" size=5
ng-model="selectedGroup"
ng-options="r.path for r in defaultGroups">
<option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-5">
<table class="table table-striped table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<div>
<label class="control-label">{{:: 'available-groups' | translate}}</label>
<kc-tooltip>{{:: 'available-groups.tooltip' | translate}}</kc-tooltip>
</div>
<div class="pull-left">
<div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteria" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
<div class="input-group-addon">
<i class="fa fa-search" id="groupSearch" ng-click="searchGroup()"></i>
</div>
</div>
</div>
&nbsp;
<button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
<div class="pull-right" data-ng-show="access.manageRealm">
<button id="addDefaultGroup" class="btn btn-default" ng-click="addDefaultGroup()">{{:: 'add' | translate}}</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div
tree-id="tree"
angular-treeview="true"
tree-model="groupList"
node-id="id"
node-label="name"
node-children="subGroups" >
</div>
</td>
</tr>
</tbody>
</table>
<div style="margin-bottom: 50px">
<kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-5">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<label class="control-label">{{:: 'available-groups' | translate}}</label>
<kc-tooltip>{{:: 'available-groups.tooltip' | translate}}</kc-tooltip>
<div class="pull-right" data-ng-show="access.manageRealm">
<button id="addDefaultGroup" class="btn btn-default" ng-click="addDefaultGroup()">{{:: 'add' | translate}}</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td> <div
tree-id="tree"
angular-treeview="true"
tree-model="groupList"
node-id="id"
node-label="name"
node-children="subGroups" >
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>
<kc-menu></kc-menu>

View file

@ -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>

View file

@ -13,14 +13,25 @@
<div class="col-md-8" >
<div class="row">
<div class="col-md-5">
<table class="table table-striped table-bordered">
<table class="table table-striped table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<label class="control-label">{{:: 'group-membership' | translate}}</label>
<kc-tooltip>{{:: 'group-membership.tooltip' | translate}}</kc-tooltip>
<div>
<label class="control-label">{{:: 'group-membership' | translate}}</label>
<kc-tooltip>{{:: 'group-membership.tooltip' | translate}}</kc-tooltip>
</div>
<div class="pull-left">
<div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteriaMembership" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
<div class="input-group-addon">
<i class="fa fa-search" id="groupSearch" ng-click="searchGroupMembership()"></i>
</div>
</div>
</div>
&nbsp;
<button id="viewAllGroups" class="btn btn-default" ng-click="clearSearchMembership()">{{:: 'view-all-groups' | translate}}</button>
<div class="pull-right" data-ng-show="user.access.manageGroupMembership">
<button id="leaveGroups" class="btn btn-default" ng-click="leaveGroup()">{{:: 'leave' | translate}}</button>
</div>
@ -31,28 +42,43 @@
<tbody>
<tr>
<td>
<select id="groupMembership" class="form-control" size=5
ng-model="selectedGroup"
ng-options="r.path for r in groupMemberships">
<option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
</select>
<div
tree-id="membershipTree"
angular-treeview="true"
tree-model="groupMemberships"
node-id="id"
node-label="name"
node-children="subGroupsMembership" >
</div>
</td>
</tr>
</tbody>
</table>
<div style="margin-bottom: 50px">
<kc-paging current-page="currentMembershipPage" number-of-pages="numberOfMembershipPages" current-page-input="currentMembershipPageInput"></kc-paging>
</div>
</div>
<div class="col-md-5">
<table class="table table-striped table-bordered">
<table class="table table-striped table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<label class="control-label">{{:: 'available-groups' | translate}}</label>
<kc-tooltip>{{:: 'membership.available-groups.tooltip' | translate}}</kc-tooltip>
<div>
<label class="control-label">{{:: 'available-groups' | translate}}</label>
<kc-tooltip>{{:: 'membership.available-groups.tooltip' | translate}}</kc-tooltip>
</div>
<div class="pull-left">
<div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteria" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
<div class="input-group-addon">
<i class="fa fa-search" id="groupSearch_availablegroups" ng-click="searchGroup()"></i>
</div>
</div>
</div>
&nbsp;
<button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
<div class="pull-right" data-ng-show="user.access.manageGroupMembership">
<button id="joinGroup" class="btn btn-default" ng-click="joinGroup()">{{:: 'join' | translate}}</button>
</div>
@ -60,21 +86,24 @@
</th>
</tr>
</thead>
<tbody>
<tr>
<td> <div
tree-id="tree"
angular-treeview="true"
tree-model="groupList"
node-id="id"
node-label="name"
node-children="subGroups" >
</div>
</td>
</tr>
<tbody>
<tr>
<td>
<div
tree-id="tree"
angular-treeview="true"
tree-model="groupList"
node-id="id"
node-label="name"
node-children="subGroups" >
</div>
</td>
</tr>
</tbody>
</table>
<div style="margin-bottom: 50px">
<kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
</div>
</div>
</div>
</div>

View file

@ -654,3 +654,4 @@ ol.setup-message li ul li {
margin-left: 24px;
margin-bottom: 8px;
}

View file

@ -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;
}