keycloak-uncached
Changes
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java 11(+11 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/user/UserGroupMembershipTest.java 157(+157 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java 95(+95 -0)
Details
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 f138d43..f00f942 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<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);
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 3a70b67..22aa8a4 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/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 63cfb65..9ee1138 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<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) {
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 23024fb..ff492f0 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
@@ -55,6 +53,11 @@ public class UserGroupMembershipEntity {
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/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index a80dec9..754229b 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -289,23 +298,79 @@ 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;
joinGroupImpl(group);
@@ -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> {
}
-
}
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 3da6525..d1439a5 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 <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);
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 33b026e..9f91721 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 <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;
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 c069f81..d6c62cd 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<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 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();
}
- return memberships;
+ Map<String, Long> 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 0000000..73d9267
--- /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 <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());
+ }
+
+}
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 beddc3a..b052c09 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<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());
});
}
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 02f1e4a..70306b5 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 81af75d..63de108 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 c51b9d5..8846754 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 = [];
+
+ $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
+ };
- 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);
+ 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 e63a200..3598a27 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 634944e..8d0866e 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 @@
- <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
- <kc-tabs-group-list></kc-tabs-group-list>
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <kc-tabs-group-list></kc-tabs-group-list>
- <form class="form-horizontal" name="realmForm" novalidate>
- <div class="form-group" kc-read-only="!access.manageRealm">
- <label class="col-md-1 control-label" class="control-label"></label>
+ <form class="form-horizontal" name="realmForm" novalidate>
+ <div class="form-group" kc-read-only="!access.manageRealm">
+ <label class="col-md-1 control-label" class="control-label"></label>
- <div class="col-md-8" >
- <div class="row">
- <div class="col-md-5">
- <table class="table table-striped table-bordered">
- <thead>
- <tr>
- <th class="kc-table-actions" colspan="5">
- <div class="form-inline">
- <label class="control-label">{{:: 'default-groups' | translate}}</label>
- <kc-tooltip>{{:: 'default-groups.tooltip' | translate}}</kc-tooltip>
+ <div class="col-md-8" >
+ <div class="row">
+ <div class="col-md-5">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <label class="control-label">{{:: 'default-groups' | translate}}</label>
+ <kc-tooltip>{{:: 'default-groups.tooltip' | translate}}</kc-tooltip>
- <div class="pull-right" data-ng-show="access.manageRealm">
- <button id="removeDefaultGroup" class="btn btn-default" ng-click="removeDefaultGroup()">{{:: 'remove' | translate}}</button>
- </div>
- </div>
- </th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>
- <select id="defaultGroups" class="form-control" size=5
- ng-model="selectedGroup"
- ng-options="r.path for r in defaultGroups">
- <option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
- </select>
+ <div class="pull-right" data-ng-show="access.manageRealm">
+ <button id="removeDefaultGroup" class="btn btn-default" ng-click="removeDefaultGroup()">{{:: 'remove' | translate}}</button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <select id="defaultGroups" class="form-control" size=5
+ ng-model="selectedGroup"
+ ng-options="r.path for r in defaultGroups">
+ <option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
+ </select>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-md-5">
+ <table class="table table-striped table-bordered" style="margin-bottom: 0">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <div>
+ <label class="control-label">{{:: 'available-groups' | translate}}</label>
+ <kc-tooltip>{{:: 'available-groups.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="pull-left">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteria" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
+ <div class="input-group-addon">
+ <i class="fa fa-search" id="groupSearch" ng-click="searchGroup()"></i>
+ </div>
+ </div>
+ </div>
+
+ <button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
+ <div class="pull-right" data-ng-show="access.manageRealm">
+ <button id="addDefaultGroup" class="btn btn-default" ng-click="addDefaultGroup()">{{:: 'add' | translate}}</button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <div
+ tree-id="tree"
+ angular-treeview="true"
+ tree-model="groupList"
+ node-id="id"
+ node-label="name"
+ node-children="subGroups" >
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div style="margin-bottom: 50px">
+ <kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
-
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <div class="col-md-5">
- <table class="table table-striped table-bordered">
- <thead>
- <tr>
- <th class="kc-table-actions" colspan="5">
-
- <div class="form-inline">
- <label class="control-label">{{:: 'available-groups' | translate}}</label>
- <kc-tooltip>{{:: 'available-groups.tooltip' | translate}}</kc-tooltip>
-
- <div class="pull-right" data-ng-show="access.manageRealm">
- <button id="addDefaultGroup" class="btn btn-default" ng-click="addDefaultGroup()">{{:: 'add' | translate}}</button>
- </div>
- </div>
- </th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td> <div
- tree-id="tree"
- angular-treeview="true"
- tree-model="groupList"
- node-id="id"
- node-label="name"
- node-children="subGroups" >
- </div>
-
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
- </form>
- </div>
-
-<kc-menu></kc-menu>
\ No newline at end of file
+<kc-menu></kc-menu>
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 7ad6f65..97a568d 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 @@
<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>
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 f7bf04e..7504108 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 @@
<div class="col-md-8" >
<div class="row">
<div class="col-md-5">
- <table class="table table-striped table-bordered">
+ <table class="table table-striped table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
- <label class="control-label">{{:: 'group-membership' | translate}}</label>
- <kc-tooltip>{{:: 'group-membership.tooltip' | translate}}</kc-tooltip>
-
+ <div>
+ <label class="control-label">{{:: 'group-membership' | translate}}</label>
+ <kc-tooltip>{{:: 'group-membership.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="pull-left">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteriaMembership" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
+ <div class="input-group-addon">
+ <i class="fa fa-search" id="groupSearch" ng-click="searchGroupMembership()"></i>
+ </div>
+ </div>
+ </div>
+
+ <button id="viewAllGroups" class="btn btn-default" ng-click="clearSearchMembership()">{{:: 'view-all-groups' | translate}}</button>
<div class="pull-right" data-ng-show="user.access.manageGroupMembership">
<button id="leaveGroups" class="btn btn-default" ng-click="leaveGroup()">{{:: 'leave' | translate}}</button>
</div>
@@ -31,28 +42,43 @@
<tbody>
<tr>
<td>
- <select id="groupMembership" class="form-control" size=5
- ng-model="selectedGroup"
- ng-options="r.path for r in groupMemberships">
- <option style="display:none" value="">{{:: 'select-a-type.placeholder' | translate}}</option>
- </select>
-
-
+ <div
+ tree-id="membershipTree"
+ angular-treeview="true"
+ tree-model="groupMemberships"
+ node-id="id"
+ node-label="name"
+ node-children="subGroupsMembership" >
+ </div>
</td>
</tr>
</tbody>
</table>
+ <div style="margin-bottom: 50px">
+ <kc-paging current-page="currentMembershipPage" number-of-pages="numberOfMembershipPages" current-page-input="currentMembershipPageInput"></kc-paging>
+ </div>
</div>
<div class="col-md-5">
- <table class="table table-striped table-bordered">
+ <table class="table table-striped table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
- <label class="control-label">{{:: 'available-groups' | translate}}</label>
- <kc-tooltip>{{:: 'membership.available-groups.tooltip' | translate}}</kc-tooltip>
-
+ <div>
+ <label class="control-label">{{:: 'available-groups' | translate}}</label>
+ <kc-tooltip>{{:: 'membership.available-groups.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="pull-left">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" ng-model="searchCriteria" class="form-control search" onkeydown="if (event.keyCode == 13) document.getElementById('groupSearch').click()">
+ <div class="input-group-addon">
+ <i class="fa fa-search" id="groupSearch_availablegroups" ng-click="searchGroup()"></i>
+ </div>
+ </div>
+ </div>
+
+ <button id="viewAllGroups" class="btn btn-default" ng-click="clearSearch()">{{:: 'view-all-groups' | translate}}</button>
<div class="pull-right" data-ng-show="user.access.manageGroupMembership">
<button id="joinGroup" class="btn btn-default" ng-click="joinGroup()">{{:: 'join' | translate}}</button>
</div>
@@ -60,21 +86,24 @@
</th>
</tr>
</thead>
- <tbody>
- <tr>
- <td> <div
- tree-id="tree"
- angular-treeview="true"
- tree-model="groupList"
- node-id="id"
- node-label="name"
- node-children="subGroups" >
- </div>
-
- </td>
- </tr>
+ <tbody>
+ <tr>
+ <td>
+ <div
+ tree-id="tree"
+ angular-treeview="true"
+ tree-model="groupList"
+ node-id="id"
+ node-label="name"
+ node-children="subGroups" >
+ </div>
+ </td>
+ </tr>
</tbody>
</table>
+ <div style="margin-bottom: 50px">
+ <kc-paging current-page="currentPage" number-of-pages="numberOfPages" current-page-input="currentPageInput"></kc-paging>
+ </div>
</div>
</div>
</div>
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 8b0a4d3..1fcd20d 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;
+}
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 dee6898..3915d8c 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;
}
+