diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java index f138d431aa..f00f9423bb 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java @@ -57,6 +57,17 @@ public interface UserResource { @GET List groups(); + @Path("groups") + @GET + List groups(@QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults); + + @Path("groups") + @GET + List groups(@QueryParam("search") String search, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults); + @Path("groups/{groupId}") @PUT void joinGroup(@PathParam("groupId") String groupId); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 63cfb65d3d..9ee11389bd 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -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 getGroups() { if (updated != null) return updated.getGroups(); - Set groups = new HashSet(); + Set groups = new LinkedHashSet<>(); for (String id : cached.getGroups(modelSupplier)) { GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm); if (groupModel == null) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java index 3a70b67e89..22aa8a4050 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java @@ -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() { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index a80dec947b..754229b1fb 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -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 Bill Burke @@ -289,22 +298,78 @@ public class UserAdapter implements UserModel, JpaModel { user.setEmailVerified(verified); } - @Override - public Set getGroups() { + private TypedQuery 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 query = em.createNamedQuery("userGroupIds", String.class); - query.setParameter("user", getEntity()); - List ids = query.getResultList(); - Set groups = new HashSet<>(); - for (String groupId : ids) { - GroupModel group = realm.getGroupById(groupId); - if (group == null) continue; - groups.add(group); + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery queryBuilder = builder.createQuery(String.class); + Root root = queryBuilder.from(UserGroupMembershipEntity.class); + + List predicates = new ArrayList<>(); + predicates.add(builder.equal(root.get("user"), getEntity())); + Join 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 query = em.createQuery(queryBuilder); + if (Objects.nonNull(first) && Objects.nonNull(max)) { + query.setFirstResult(first).setMaxResults(max); + } + return query; + } + + private TypedQuery 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 queryBuilder = builder.createQuery(Long.class); + Root root = queryBuilder.from(UserGroupMembershipEntity.class); + + List predicates = new ArrayList<>(); + predicates.add(builder.equal(root.get("user"), getEntity())); + if (Objects.nonNull(search) && !search.isEmpty()) { + Join 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 getGroupModels(Collection groupIds) { + Set groups = new LinkedHashSet<>(); + for (String id : groupIds) { + groups.add(realm.getGroupById(id)); } return groups; } + @Override + public Set getGroups() { + return getGroupModels(createGetGroupsQuery(null, null, null).getResultList()); + } + + @Override + public Set 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 { public boolean hasRole(RoleModel role) { Set roles = getRoleMappings(); return RoleUtils.hasRole(roles, role) - || RoleUtils.hasRoleFromGroup(getGroups(), role, true); + || RoleUtils.hasRoleFromGroup(getGroups(), role, true); } protected TypedQuery getUserRoleMappingEntityTypedQuery(RoleModel role) { @@ -431,9 +496,9 @@ public class UserAdapter implements UserModel, JpaModel { 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 { } - } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java index 23024fbf33..ff492f0b54 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserGroupMembershipEntity.java @@ -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; diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 33b026e994..9f91721710 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -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 Bill Burke @@ -102,6 +106,12 @@ public class ModelToRepresentation { return result; } + public static List 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 toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) { List hierarchy = new LinkedList<>(); List groups = realm.getTopLevelGroups(first, max); @@ -113,6 +123,12 @@ public class ModelToRepresentation { return hierarchy; } + public static List 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 toGroupHierarchy(RealmModel realm, boolean full) { List hierarchy = new LinkedList<>(); List groups = realm.getTopLevelGroups(); @@ -124,6 +140,12 @@ public class ModelToRepresentation { return hierarchy; } + public static List 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 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()); 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 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; diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java index 3da65250ef..d1439a5353 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -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 Bill Burke @@ -112,6 +114,32 @@ public interface UserModel extends RoleMapperModel { void setEmailVerified(boolean verified); Set getGroups(); + + default Set getGroups(int first, int max) { + return getGroups(null, first, max); + } + + default Set 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); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index c069f81639..d6c62cdff6 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -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 groupMembership() { + public List groupMembership(@QueryParam("search") String search, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults) { auth.users().requireView(user); - List memberships = new LinkedList<>(); - for (GroupModel group : user.getGroups()) { - memberships.add(ModelToRepresentation.toRepresentation(group, false)); + List 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 getGroupMembershipCount(@QueryParam("search") String search) { + auth.users().requireView(user); + Long results; + + if (Objects.nonNull(search)) { + results = user.getGroupsCountByNameContaining(search); + } else { + results = user.getGroupsCount(); + } + Map map = new HashMap<>(); + map.put("count", results); + return map; } @DELETE diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/user/UserGroupMembershipTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/user/UserGroupMembershipTest.java new file mode 100755 index 0000000000..73d9267608 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/user/UserGroupMembershipTest.java @@ -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 Volker Suschke + * @author Leon Graser + */ +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 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 groups = realm.users().get(userId).groups("-3", 0, 10); + assertEquals(1, groups.size()); + assertNames(groups, "group-3"); + + List groups2 = realm.users().get(userId).groups("1", 0, 10); + assertEquals(2, groups2.size()); + assertNames(groups2, "group-1", "group-10"); + + List groups3 = realm.users().get(userId).groups("1", 2, 10); + assertEquals(0, groups3.size()); + + List groups4 = realm.users().get(userId).groups("gr", 2, 10); + assertEquals(8, groups4.size()); + + List groups5 = realm.users().get(userId).groups("Gr", 2, 10); + assertEquals(8, groups5.size()); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java index beddc3a66e..b052c09f23 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java @@ -128,6 +128,21 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest { UserModel johnDb = session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm); Set johnDbGroups = johnDb.getGroups(); Assert.assertEquals(2, johnDbGroups.size()); + + Set johnDbGroupsWithGr = johnDb.getGroups("Gr", 0, 10); + Assert.assertEquals(2, johnDbGroupsWithGr.size()); + + Set johnDbGroupsWithGr2 = johnDb.getGroups("Gr", 1, 10); + Assert.assertEquals(1, johnDbGroupsWithGr2.size()); + + Set johnDbGroupsWithGr3 = johnDb.getGroups("Gr", 0, 1); + Assert.assertEquals(1, johnDbGroupsWithGr3.size()); + + Set 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 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 johnGroupsWithGr = john.getGroups("gr", 0, 10); + Assert.assertEquals(2, johnGroupsWithGr.size()); + + Set johnGroupsWithGr2 = john.getGroups("gr", 1, 10); + Assert.assertEquals(1, johnGroupsWithGr2.size()); + + Set johnGroupsWithGr3 = john.getGroups("gr", 0, 1); + Assert.assertEquals(1, johnGroupsWithGr3.size()); + + Set johnGroupsWith12 = john.getGroups("12", 0, 10); + Assert.assertEquals(1, johnGroupsWith12.size()); + // 4 - Check through userProvider List group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10); List 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 maryGroupsWithGr = mary.getGroups("gr", 0, 10); + Assert.assertEquals(5, maryGroupsWithGr.size()); + + Set maryGroupsWithGr2 = mary.getGroups("gr", 1, 10); + Assert.assertEquals(4, maryGroupsWithGr2.size()); + + Set maryGroupsWithGr3 = mary.getGroups("gr", 0, 1); + Assert.assertEquals(1, maryGroupsWithGr3.size()); + + Set 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 maryDBGroupsWithGr = maryDB.getGroups("Gr", 0, 10); + Assert.assertEquals(3, maryDBGroupsWithGr.size()); + + Set maryDBGroupsWithGr2 = maryDB.getGroups("Gr", 1, 10); + Assert.assertEquals(2, maryDBGroupsWithGr2.size()); + + Set maryDBGroupsWithGr3 = maryDB.getGroups("Gr", 0, 1); + Assert.assertEquals(1, maryDBGroupsWithGr3.size()); + + Set 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 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 robGroupsWithGr = rob.getGroups("Gr", 0, 10); + Assert.assertEquals(4, robGroupsWithGr.size()); + + Set robGroupsWithGr2 = rob.getGroups("Gr", 1, 10); + Assert.assertEquals(3, robGroupsWithGr2.size()); + + Set robGroupsWithGr3 = rob.getGroups("Gr", 0, 1); + Assert.assertEquals(1, robGroupsWithGr3.size()); + + Set 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 groupsWith3v1 = john.getGroups("3", 0, 10); + Assert.assertEquals(2, groupsWith3v1.size()); + + Set groupsWith3v2 = john.getGroups("3", 1, 10); + Assert.assertEquals(1, groupsWith3v2.size()); + + Set groupsWith3v3 = john.getGroups("3", 1, 1); + Assert.assertEquals(1, groupsWith3v3.size()); + + Set groupsWith3v4 = john.getGroups("3", 1, 0); + Assert.assertEquals(0, groupsWith3v4.size()); + + Set groupsWithKeycloak = john.getGroups("Keycloak", 0, 10); + Assert.assertEquals(0, groupsWithKeycloak.size()); }); } diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js index 02f1e4a455..70306b5fc3 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -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' diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js index 81af75d400..63de108d65 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js @@ -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(); }); }; diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index c51b9d5529..8846754f06 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -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; diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js index e63a2009df..3598a2744a 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -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) + } + } + }); +}); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html b/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html index 634944eee9..8d0866e7ae 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html @@ -1,80 +1,91 @@ -
- +
+ -
-
- + +
+ -
-
-
- - - - - -
-
- - {{:: 'default-groups.tooltip' | translate}} +
+
+
+ + + + - - - - - + + + + + + +
+
+ + {{:: 'default-groups.tooltip' | translate}} -
- -
-
-
- +
+ +
+ + +
+ +
+
+
+ + + + + + + + + + + +
+
+
+ + {{:: 'available-groups.tooltip' | translate}} +
+
+
+ +
+ +
+
+
+   + +
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+ + - - -
-
-
- - - - - - - - - - - -
- -
- - {{:: 'available-groups.tooltip' | translate}} - -
- -
-
-
-
- -
-
-
-
-
- -
- - \ No newline at end of file + diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html index 7ad6f657b4..97a568d998 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/group-list.html @@ -8,7 +8,7 @@
- +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html index f7bf04e458..750410838b 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html @@ -13,14 +13,25 @@
- +
- - {{:: 'group-membership.tooltip' | translate}} - +
+ + {{:: 'group-membership.tooltip' | translate}} +
+
+
+ +
+ +
+
+
+   +
@@ -31,28 +42,43 @@
- - - +
+
+
+ +
- +
- - - - + + + +
- - {{:: 'membership.available-groups.tooltip' | translate}} - +
+ + {{:: 'membership.available-groups.tooltip' | translate}} +
+
+
+ +
+ +
+
+
+   +
@@ -60,21 +86,24 @@
-
- -
+
+
+
+
+ +
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css b/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css index dee6898ed7..3915d8ca45 100644 --- a/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css +++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css @@ -654,3 +654,4 @@ ol.setup-message li ul li { margin-left: 24px; margin-bottom: 8px; } + diff --git a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css index 8b0a4d36ac..1fcd20dd89 100755 --- a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css +++ b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css @@ -404,4 +404,17 @@ table.kc-authz-table-expanded { .password-conceal { font-family: 'text-security-disc'; font-size: 14px; -} \ No newline at end of file +} + +/* 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; +}