From 01a42f417fbac59204901e9c534a3622b2db1a2e Mon Sep 17 00:00:00 2001 From: Leon Graser Date: Fri, 16 Aug 2019 16:09:03 +0200 Subject: [PATCH] Search and Filter for the count endpoint --- .../admin/client/resource/UsersResource.java | 35 ++ .../cache/infinispan/UserCacheSession.java | 25 ++ .../keycloak/models/jpa/JpaUserProvider.java | 128 ++++++- .../models/jpa/entities/UserEntity.java | 2 + .../entities/UserGroupMembershipEntity.java | 5 +- .../storage/user/UserQueryProvider.java | 99 +++++- .../resources/admin/UsersResource.java | 65 +++- .../keycloak/storage/UserStorageManager.java | 25 ++ .../keycloak/testsuite/admin/UsersTest.java | 322 ++++++++++++++++++ 9 files changed, 699 insertions(+), 7 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java index dfac03fca2..877e38a6f0 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java @@ -111,11 +111,46 @@ public interface UsersResource { @Consumes(MediaType.APPLICATION_JSON) Response create(UserRepresentation userRepresentation); + /** + * Returns the number of users that can be viewed. + * + * @return number of users + */ @Path("count") @GET @Produces(MediaType.APPLICATION_JSON) Integer count(); + /** + * Returns the number of users that can be viewed and match the given search criteria. + * If none is specified this is equivalent to {{@link #count()}}. + * + * @param search criteria to search for + * @return number of users matching the search criteria + */ + @Path("count") + @GET + @Produces(MediaType.APPLICATION_JSON) + Integer count(@QueryParam("search") String search); + + /** + * Returns the number of users that can be viewed and match the given filters. + * If none of the filters is specified this is equivalent to {{@link #count()}}. + * + * @param last last name field of a user + * @param first first name field of a user + * @param email email field of a user + * @param username username field of a user + * @return number of users matching the given filters + */ + @Path("count") + @GET + @Produces(MediaType.APPLICATION_JSON) + Integer count(@QueryParam("lastName") String last, + @QueryParam("firstName") String first, + @QueryParam("email") String email, + @QueryParam("username") String username); + @Path("{id}") UserResource get(@PathParam("id") String id); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 17d69fb871..9d188df09c 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -550,6 +550,31 @@ public class UserCacheSession implements UserCache { return getUsersCount(realm, false); } + @Override + public int getUsersCount(RealmModel realm, Set groupIds) { + return getDelegate().getUsersCount(realm, groupIds); + } + + @Override + public int getUsersCount(String search, RealmModel realm) { + return getDelegate().getUsersCount(search, realm); + } + + @Override + public int getUsersCount(String search, RealmModel realm, Set groupIds) { + return getDelegate().getUsersCount(search, realm, groupIds); + } + + @Override + public int getUsersCount(Map params, RealmModel realm) { + return getDelegate().getUsersCount(params, realm); + } + + @Override + public int getUsersCount(Map params, RealmModel realm, Set groupIds) { + return getDelegate().getUsersCount(params, realm, groupIds); + } + @Override public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 41617d0e88..e7cc81447d 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -52,6 +52,7 @@ import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.persistence.criteria.Subquery; @@ -65,7 +66,6 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.persistence.LockModeType; -import javax.persistence.criteria.Expression; /** * @author Bill Burke @@ -607,6 +607,132 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { return getUsersCount(realm, false); } + @Override + public int getUsersCount(RealmModel realm, Set groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return 0; + } + + TypedQuery query = em.createNamedQuery("userCountInGroups", Long.class); + query.setParameter("realmId", realm.getId()); + query.setParameter("groupIds", groupIds); + Long count = query.getSingleResult(); + + return count.intValue(); + } + + @Override + public int getUsersCount(String search, RealmModel realm) { + TypedQuery query = em.createNamedQuery("searchForUserCount", Long.class); + query.setParameter("realmId", realm.getId()); + query.setParameter("search", "%" + search.toLowerCase() + "%"); + Long count = query.getSingleResult(); + + return count.intValue(); + } + + @Override + public int getUsersCount(String search, RealmModel realm, Set groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return 0; + } + + TypedQuery query = em.createNamedQuery("searchForUserCountInGroups", Long.class); + query.setParameter("realmId", realm.getId()); + query.setParameter("search", "%" + search.toLowerCase() + "%"); + query.setParameter("groupIds", groupIds); + Long count = query.getSingleResult(); + + return count.intValue(); + } + + @Override + public int getUsersCount(Map params, RealmModel realm) { + CriteriaBuilder qb = em.getCriteriaBuilder(); + CriteriaQuery userQuery = qb.createQuery(Long.class); + Root from = userQuery.from(UserEntity.class); + Expression count = qb.count(from); + + userQuery = userQuery.select(count); + List restrictions = new ArrayList<>(); + restrictions.add(qb.equal(from.get("realmId"), realm.getId())); + + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key == null || value == null) { + continue; + } + + switch (key) { + case UserModel.USERNAME: + restrictions.add(qb.like(from.get("username"), "%" + value + "%")); + break; + case UserModel.FIRST_NAME: + restrictions.add(qb.like(from.get("firstName"), "%" + value + "%")); + break; + case UserModel.LAST_NAME: + restrictions.add(qb.like(from.get("lastName"), "%" + value + "%")); + break; + case UserModel.EMAIL: + restrictions.add(qb.like(from.get("email"), "%" + value + "%")); + break; + } + } + + userQuery = userQuery.where(restrictions.toArray(new Predicate[0])); + TypedQuery query = em.createQuery(userQuery); + Long result = query.getSingleResult(); + + return result.intValue(); + } + + @Override + public int getUsersCount(Map params, RealmModel realm, Set groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return 0; + } + + CriteriaBuilder qb = em.getCriteriaBuilder(); + CriteriaQuery userQuery = qb.createQuery(Long.class); + Root from = userQuery.from(UserGroupMembershipEntity.class); + Expression count = qb.count(from.get("user")); + userQuery = userQuery.select(count); + + List restrictions = new ArrayList<>(); + restrictions.add(qb.equal(from.get("user").get("realmId"), realm.getId())); + restrictions.add(from.get("groupId").in(groupIds)); + + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key == null || value == null) { + continue; + } + + switch (key) { + case UserModel.USERNAME: + restrictions.add(qb.like(from.get("user").get("username"), "%" + value + "%")); + break; + case UserModel.FIRST_NAME: + restrictions.add(qb.like(from.get("user").get("firstName"), "%" + value + "%")); + break; + case UserModel.LAST_NAME: + restrictions.add(qb.like(from.get("user").get("lastName"), "%" + value + "%")); + break; + case UserModel.EMAIL: + restrictions.add(qb.like(from.get("user").get("email"), "%" + value + "%")); + break; + } + } + + userQuery = userQuery.where(restrictions.toArray(new Predicate[0])); + TypedQuery query = em.createQuery(userQuery); + Long result = query.getSingleResult(); + + return result.intValue(); + } + @Override public List getUsers(RealmModel realm) { return getUsers(realm, false); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index cd12fc9258..17ba643574 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -46,6 +46,8 @@ import java.util.Collection; @NamedQuery(name="getAllUsersByRealmExcludeServiceAccount", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) order by u.username"), @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) and " + "( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), + @NamedQuery(name="searchForUserCount", query="select count(u) from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) and " + + "( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search )"), @NamedQuery(name="getRealmUserByUsername", query="select u from UserEntity u where u.username = :username and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByEmail", query="select u from UserEntity u where u.email = :email and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByLastName", query="select u from UserEntity u where u.lastName = :lastName and u.realmId = :realmId"), 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 ff492f0b54..4f350c0b44 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 @@ -40,7 +40,10 @@ import java.io.Serializable; @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") + @NamedQuery(name="deleteUserGroupMembershipsByUser", query="delete from UserGroupMembershipEntity m where m.user = :user"), + @NamedQuery(name="searchForUserCountInGroups", query="select count(m.user) from UserGroupMembershipEntity m where m.user.realmId = :realmId and (m.user.serviceAccountClientLink is null) and " + + "( lower(m.user.username) like :search or lower(concat(m.user.firstName, ' ', m.user.lastName)) like :search or m.user.email like :search ) and m.group.id in :groupIds"), + @NamedQuery(name="userCountInGroups", query="select count(m.user) from UserGroupMembershipEntity m where m.user.realmId = :realmId and m.group.id in :groupIds") }) @Table(name="USER_GROUP_MEMBERSHIP") @Entity diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java index 83c74f131b..06121d9724 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java @@ -24,6 +24,8 @@ import org.keycloak.models.UserModel; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * Optional capability interface implemented by UserStorageProviders. @@ -43,6 +45,99 @@ public interface UserQueryProvider { */ int getUsersCount(RealmModel realm); + /** + * Returns the number of users that are in at least one of the groups + * given. + * + * @param realm the realm + * @param groupIds set of groups id to check for + * @return the number of users that are in at least one of the groups + */ + default int getUsersCount(RealmModel realm, Set groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return 0; + } + + return countUsersInGroups(getUsers(realm), groupIds); + } + + /** + * Returns the number of users that match the given criteria. + * + * @param search search criteria + * @param realm the realm + * @return number of users that match the search + */ + default int getUsersCount(String search, RealmModel realm) { + return searchForUser(search, realm).size(); + } + + /** + * Returns the number of users that match the given criteria and are in + * at least one of the groups given. + * + * @param search search criteria + * @param realm the realm + * @param groupIds set of groups to check for + * @return number of users that match the search and given groups + */ + default int getUsersCount(String search, RealmModel realm, Set groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return 0; + } + + List users = searchForUser(search, realm); + return countUsersInGroups(users, groupIds); + } + + /** + * Returns the number of users that match the given filter parameters. + * + * @param params filter parameters + * @param realm the realm + * @return number of users that match the given filters + */ + default int getUsersCount(Map params, RealmModel realm) { + return searchForUser(params, realm).size(); + } + + /** + * Returns the number of users that match the given filter parameters and is in + * at least one of the given groups. + * + * @param params filter parameters + * @param realm the realm + * @param groupIds set if groups to check for + * @return number of users that match the given filters and groups + */ + default int getUsersCount(Map params, RealmModel realm, Set groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return 0; + } + + List users = searchForUser(params, realm); + return countUsersInGroups(users, groupIds); + } + + /** + * Returns the number of users from the given list of users that are in at + * least one of the groups given in the groups set. + * + * @param users list of users to check + * @param groupIds id of groups that should be checked for + * @return number of users that are in at least one of the groups + */ + static int countUsersInGroups(List users, Set groupIds) { + return (int) users.stream().filter(u -> { + for (GroupModel group : u.getGroups()) { + if (groupIds.contains(group.getId())) { + return true; + } + } + return false; + }).count(); + } + /** * Returns the number of users. * @@ -138,7 +233,7 @@ public interface UserQueryProvider { /** * Get users that belong to a specific role. - * + * * * * @param realm @@ -152,7 +247,7 @@ public interface UserQueryProvider { /** * Search for users that have a specific role with a specific roleId. - * + * * * * @param firstResult diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 551b70cbfe..67fc0dc81c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -235,14 +235,73 @@ public class UsersResource { return toRepresentation(realm, userPermissionEvaluator, briefRepresentation, userModels); } + /** + * Returns the number of users that match the given criteria. + * It can be called in three different ways. + * 1. Don't specify any criteria and pass {@code null}. The number of all + * users within that realm will be returned. + *

+ * 2. If {@code search} is specified other criteria such as {@code last} will + * be ignored even though you set them. The {@code search} string will be + * matched against the first and last name, the username and the email of a + * user. + *

+ * 3. If {@code search} is unspecified but any of {@code last}, {@code first}, + * {@code email} or {@code username} those criteria are matched against their + * respective fields on a user entity. Combined with a logical and. + * + * @param search arbitrary search string for all the fields below + * @param last last name filter + * @param first first name filter + * @param email email filter + * @param username username filter + * @return the number of users that match the given criteria + */ @Path("count") @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Integer getUsersCount() { - auth.users().requireView(); + public Integer getUsersCount(@QueryParam("search") String search, + @QueryParam("lastName") String last, + @QueryParam("firstName") String first, + @QueryParam("email") String email, + @QueryParam("username") String username) { + UserPermissionEvaluator userPermissionEvaluator = auth.users(); + userPermissionEvaluator.requireQuery(); - return session.users().getUsersCount(realm); + if (search != null) { + if (search.startsWith(SEARCH_ID_PARAMETER)) { + UserModel userModel = session.users().getUserById(search.substring(SEARCH_ID_PARAMETER.length()).trim(), realm); + return userModel != null && userPermissionEvaluator.canView(userModel) ? 1 : 0; + } else if (userPermissionEvaluator.canView()) { + return session.users().getUsersCount(search.trim(), realm); + } else { + return session.users().getUsersCount(search.trim(), realm, auth.groups().getGroupsWithViewPermission()); + } + } else if (last != null || first != null || email != null || username != null) { + Map parameters = new HashMap<>(); + if (last != null) { + parameters.put(UserModel.LAST_NAME, last); + } + if (first != null) { + parameters.put(UserModel.FIRST_NAME, first); + } + if (email != null) { + parameters.put(UserModel.EMAIL, email); + } + if (username != null) { + parameters.put(UserModel.USERNAME, username); + } + if (userPermissionEvaluator.canView()) { + return session.users().getUsersCount(parameters, realm); + } else { + return session.users().getUsersCount(parameters, realm, auth.groups().getGroupsWithViewPermission()); + } + } else if (userPermissionEvaluator.canView()) { + return session.users().getUsersCount(realm); + } else { + return session.users().getUsersCount(realm, auth.groups().getGroupsWithViewPermission()); + } } private List searchForUser(Map attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) { diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 7403d5f8b1..0cc2756a1a 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -462,6 +462,31 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo return getUsersCount(realm, false); } + @Override + public int getUsersCount(RealmModel realm, Set groupIds) { + return localStorage().getUsersCount(realm, groupIds); + } + + @Override + public int getUsersCount(String search, RealmModel realm) { + return localStorage().getUsersCount(search, realm); + } + + @Override + public int getUsersCount(String search, RealmModel realm, Set groupIds) { + return localStorage().getUsersCount(search, realm, groupIds); + } + + @Override + public int getUsersCount(Map params, RealmModel realm) { + return localStorage().getUsersCount(params, realm); + } + + @Override + public int getUsersCount(Map params, RealmModel realm, Set groupIds) { + return localStorage().getUsersCount(params, realm, groupIds); + } + @FunctionalInterface interface PaginatedQuery { List query(Object provider, int first, int max); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java new file mode 100644 index 0000000000..fece331182 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java @@ -0,0 +1,322 @@ +/* + * Copyright 2019 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; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.ManagementPermissionRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; +import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; +import org.keycloak.testsuite.util.AdminClientUtil; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class UsersTest extends AbstractAdminTest { + + @Before + public void cleanUsers() { + List userRepresentations = realm.users().list(); + for (UserRepresentation user : userRepresentations) { + realm.users().delete(user.getId()); + } + } + + @Test + public void countUsersWithViewPermission() { + createUser(realmId, "user1", "password", "user1FirstName", "user1LastName", "user1@example.com"); + createUser(realmId, "user2", "password", "user2FirstName", "user2LastName", "user2@example.com"); + assertThat(realm.users().count(), is(2)); + } + + @Test + public void countUsersBySearchWithViewPermission() { + createUser(realmId, "user1", "password", "user1FirstName", "user1LastName", "user1@example.com"); + createUser(realmId, "user2", "password", "user2FirstName", "user2LastName", "user2@example.com"); + //search all + assertThat(realm.users().count("user"), is(2)); + //search first name + assertThat(realm.users().count("FirstName"), is(2)); + assertThat(realm.users().count("user2FirstName"), is(1)); + //search last name + assertThat(realm.users().count("LastName"), is(2)); + assertThat(realm.users().count("user2LastName"), is(1)); + //search in email + assertThat(realm.users().count("@example.com"), is(2)); + assertThat(realm.users().count("user1@example.com"), is(1)); + //search for something not existing + assertThat(realm.users().count("notExisting"), is(0)); + //search for empty string + assertThat(realm.users().count(""), is(2)); + //search not specified (defaults to simply /count) + assertThat(realm.users().count(null), is(2)); + } + + @Test + public void countUsersByFiltersWithViewPermission() { + createUser(realmId, "user1", "password", "user1FirstName", "user1LastName", "user1@example.com"); + createUser(realmId, "user2", "password", "user2FirstName", "user2LastName", "user2@example.com"); + //search username + assertThat(realm.users().count(null, null, null, "user"), is(2)); + assertThat(realm.users().count(null, null, null, "user1"), is(1)); + assertThat(realm.users().count(null, null, null, "notExisting"), is(0)); + assertThat(realm.users().count(null, null, null, ""), is(2)); + //search first name + assertThat(realm.users().count(null, "FirstName", null, null), is(2)); + assertThat(realm.users().count(null, "user2FirstName", null, null), is(1)); + assertThat(realm.users().count(null, "notExisting", null, null), is(0)); + assertThat(realm.users().count(null, "", null, null), is(2)); + //search last name + assertThat(realm.users().count("LastName", null, null, null), is(2)); + assertThat(realm.users().count("user2LastName", null, null, null), is(1)); + assertThat(realm.users().count("notExisting", null, null, null), is(0)); + assertThat(realm.users().count("", null, null, null), is(2)); + //search in email + assertThat(realm.users().count(null, null, "@example.com", null), is(2)); + assertThat(realm.users().count(null, null, "user1@example.com", null), is(1)); + assertThat(realm.users().count(null, null, "user1@test.com", null), is(0)); + assertThat(realm.users().count(null, null, "", null), is(2)); + //search for combinations + assertThat(realm.users().count("LastName", "FirstName", null, null), is(2)); + assertThat(realm.users().count("user1LastName", "FirstName", null, null), is(1)); + assertThat(realm.users().count("user1LastName", "", null, null), is(1)); + assertThat(realm.users().count("LastName", "", null, null), is(2)); + assertThat(realm.users().count("LastName", "", null, null), is(2)); + assertThat(realm.users().count(null, null, "@example.com", "user"), is(2)); + //search not specified (defaults to simply /count) + assertThat(realm.users().count(null, null, null, null), is(2)); + assertThat(realm.users().count("", "", "", ""), is(2)); + } + + @Test + public void countUsersWithGroupViewPermission() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException { + RealmResource testRealmResource = setupTestEnvironmentWithPermissions(true); + assertThat(testRealmResource.users().count(), is(3)); + } + + @Test + public void countUsersBySearchWithGroupViewPermission() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException { + RealmResource testRealmResource = setupTestEnvironmentWithPermissions(true); + //search all + assertThat(testRealmResource.users().count("user"), is(3)); + //search first name + assertThat(testRealmResource.users().count("FirstName"), is(3)); + assertThat(testRealmResource.users().count("user2FirstName"), is(1)); + //search last name + assertThat(testRealmResource.users().count("LastName"), is(3)); + assertThat(testRealmResource.users().count("user2LastName"), is(1)); + //search in email + assertThat(testRealmResource.users().count("@example.com"), is(3)); + assertThat(testRealmResource.users().count("user1@example.com"), is(1)); + //search for something not existing + assertThat(testRealmResource.users().count("notExisting"), is(0)); + //search for empty string + assertThat(testRealmResource.users().count(""), is(3)); + //search not specified (defaults to simply /count) + assertThat(testRealmResource.users().count(null), is(3)); + } + + @Test + public void countUsersByFiltersWithGroupViewPermission() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException { + RealmResource testRealmResource = setupTestEnvironmentWithPermissions(true); + //search username + assertThat(testRealmResource.users().count(null, null, null, "user"), is(3)); + assertThat(testRealmResource.users().count(null, null, null, "user1"), is(1)); + assertThat(testRealmResource.users().count(null, null, null, "notExisting"), is(0)); + assertThat(testRealmResource.users().count(null, null, null, ""), is(3)); + //search first name + assertThat(testRealmResource.users().count(null, "FirstName", null, null), is(3)); + assertThat(testRealmResource.users().count(null, "user2FirstName", null, null), is(1)); + assertThat(testRealmResource.users().count(null, "notExisting", null, null), is(0)); + assertThat(testRealmResource.users().count(null, "", null, null), is(3)); + //search last name + assertThat(testRealmResource.users().count("LastName", null, null, null), is(3)); + assertThat(testRealmResource.users().count("user2LastName", null, null, null), is(1)); + assertThat(testRealmResource.users().count("notExisting", null, null, null), is(0)); + assertThat(testRealmResource.users().count("", null, null, null), is(3)); + //search in email + assertThat(testRealmResource.users().count(null, null, "@example.com", null), is(3)); + assertThat(testRealmResource.users().count(null, null, "user1@example.com", null), is(1)); + assertThat(testRealmResource.users().count(null, null, "user1@test.com", null), is(0)); + assertThat(testRealmResource.users().count(null, null, "", null), is(3)); + //search for combinations + assertThat(testRealmResource.users().count("LastName", "FirstName", null, null), is(3)); + assertThat(testRealmResource.users().count("user1LastName", "FirstName", null, null), is(1)); + assertThat(testRealmResource.users().count("user1LastName", "", null, null), is(1)); + assertThat(testRealmResource.users().count("LastName", "", null, null), is(3)); + assertThat(testRealmResource.users().count("LastName", "", null, null), is(3)); + assertThat(testRealmResource.users().count(null, null, "@example.com", "user"), is(3)); + //search not specified (defaults to simply /count) + assertThat(testRealmResource.users().count(null, null, null, null), is(3)); + assertThat(testRealmResource.users().count("", "", "", ""), is(3)); + } + + @Test + public void countUsersWithNoViewPermission() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { + RealmResource testRealmResource = setupTestEnvironmentWithPermissions(false); + assertThat(testRealmResource.users().count(), is(0)); + } + + @Test + public void countUsersBySearchWithNoViewPermission() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException { + RealmResource testRealmResource = setupTestEnvironmentWithPermissions(false); + //search all + assertThat(testRealmResource.users().count("user"), is(0)); + //search first name + assertThat(testRealmResource.users().count("FirstName"), is(0)); + assertThat(testRealmResource.users().count("user2FirstName"), is(0)); + //search last name + assertThat(testRealmResource.users().count("LastName"), is(0)); + assertThat(testRealmResource.users().count("user2LastName"), is(0)); + //search in email + assertThat(testRealmResource.users().count("@example.com"), is(0)); + assertThat(testRealmResource.users().count("user1@example.com"), is(0)); + //search for something not existing + assertThat(testRealmResource.users().count("notExisting"), is(0)); + //search for empty string + assertThat(testRealmResource.users().count(""), is(0)); + //search not specified (defaults to simply /count) + assertThat(testRealmResource.users().count(null), is(0)); + } + + @Test + public void countUsersByFiltersWithNoViewPermission() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException { + RealmResource testRealmResource = setupTestEnvironmentWithPermissions(false); + //search username + assertThat(testRealmResource.users().count(null, null, null, "user"), is(0)); + assertThat(testRealmResource.users().count(null, null, null, "user1"), is(0)); + assertThat(testRealmResource.users().count(null, null, null, "notExisting"), is(0)); + assertThat(testRealmResource.users().count(null, null, null, ""), is(0)); + //search first name + assertThat(testRealmResource.users().count(null, "FirstName", null, null), is(0)); + assertThat(testRealmResource.users().count(null, "user2FirstName", null, null), is(0)); + assertThat(testRealmResource.users().count(null, "notExisting", null, null), is(0)); + assertThat(testRealmResource.users().count(null, "", null, null), is(0)); + //search last name + assertThat(testRealmResource.users().count("LastName", null, null, null), is(0)); + assertThat(testRealmResource.users().count("user2LastName", null, null, null), is(0)); + assertThat(testRealmResource.users().count("notExisting", null, null, null), is(0)); + assertThat(testRealmResource.users().count("", null, null, null), is(0)); + //search in email + assertThat(testRealmResource.users().count(null, null, "@example.com", null), is(0)); + assertThat(testRealmResource.users().count(null, null, "user1@example.com", null), is(0)); + assertThat(testRealmResource.users().count(null, null, "user1@test.com", null), is(0)); + assertThat(testRealmResource.users().count(null, null, "", null), is(0)); + //search for combinations + assertThat(testRealmResource.users().count("LastName", "FirstName", null, null), is(0)); + assertThat(testRealmResource.users().count("user1LastName", "FirstName", null, null), is(0)); + assertThat(testRealmResource.users().count("user1LastName", "", null, null), is(0)); + assertThat(testRealmResource.users().count("LastName", "", null, null), is(0)); + assertThat(testRealmResource.users().count("LastName", "", null, null), is(0)); + assertThat(testRealmResource.users().count(null, null, "@example.com", "user"), is(0)); + //search not specified (defaults to simply /count) + assertThat(testRealmResource.users().count(null, null, null, null), is(0)); + assertThat(testRealmResource.users().count("", "", "", ""), is(0)); + } + + private RealmResource setupTestEnvironmentWithPermissions(boolean grp1ViewPermissions) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException { + String testUserId = createUser(realmId, "test-user", "password", "", "", ""); + //assign 'query-users' role to test user + ClientRepresentation clientRepresentation = realm.clients().findByClientId("realm-management").get(0); + String realmManagementId = clientRepresentation.getId(); + RoleRepresentation roleRepresentation = realm.clients().get(realmManagementId).roles().get("query-users").toRepresentation(); + realm.users().get(testUserId).roles().clientLevel(realmManagementId).add(Collections.singletonList(roleRepresentation)); + + //create test users and groups + List groups = setupUsersInGroupsWithPermissions(); + + if (grp1ViewPermissions) { + AuthorizationResource authorizationResource = realm.clients().get(realmManagementId).authorization(); + //create a user policy for the test user + UserPolicyRepresentation policy = new UserPolicyRepresentation(); + String policyName = "test-policy"; + policy.setName(policyName); + policy.setUsers(Collections.singleton(testUserId)); + authorizationResource.policies().user().create(policy); + PolicyRepresentation policyRepresentation = authorizationResource.policies().findByName(policyName); + //add the policy to grp1 + Optional optional = groups.stream().filter(g -> g.getName().equals("grp1")).findFirst(); + assertThat(optional.isPresent(), is(true)); + GroupRepresentation grp1 = optional.get(); + ScopePermissionRepresentation scopePermissionRepresentation = authorizationResource.permissions().scope().findByName("view.members.permission.group." + grp1.getId()); + scopePermissionRepresentation.setPolicies(Collections.singleton(policyRepresentation.getId())); + scopePermissionRepresentation.setDecisionStrategy(DecisionStrategy.UNANIMOUS); + authorizationResource.permissions().scope().findById(scopePermissionRepresentation.getId()).update(scopePermissionRepresentation); + } + + Keycloak testUserClient = AdminClientUtil.createAdminClient(true, realm.toRepresentation().getRealm(), "test-user", "password", "admin-cli", ""); + + return testUserClient.realm(realm.toRepresentation().getRealm()); + } + + private List setupUsersInGroupsWithPermissions() { + //create two groups + GroupRepresentation grp1 = createGroupWithPermissions("grp1"); + GroupRepresentation grp2 = createGroupWithPermissions("grp2"); + //create test users + String user1Id = createUser(realmId, "user1", "password", "user1FirstName", "user1LastName", "user1@example.com"); + String user2Id = createUser(realmId, "user2", "password", "user2FirstName", "user2LastName", "user2@example.com"); + String user3Id = createUser(realmId, "user3", "password", "user3FirstName", "user3LastName", "user3@example.com"); + String user4Id = createUser(realmId, "user4", "password", "user4FirstName", "user4LastName", "user4@example.com"); + //add users to groups + realm.users().get(user1Id).joinGroup(grp1.getId()); + realm.users().get(user2Id).joinGroup(grp1.getId()); + realm.users().get(user3Id).joinGroup(grp1.getId()); + realm.users().get(user4Id).joinGroup(grp2.getId()); + + List groups = new ArrayList<>(); + groups.add(grp1); + groups.add(grp2); + + return groups; + } + + private GroupRepresentation createGroupWithPermissions(String name) { + GroupRepresentation grp = new GroupRepresentation(); + grp.setName(name); + realm.groups().add(grp); + Optional optional = realm.groups().groups().stream().filter(g -> g.getName().equals(name)).findFirst(); + assertThat(optional.isPresent(), is(true)); + grp = optional.get(); + String id = grp.getId(); + //enable the permissions + realm.groups().group(id).setPermissions(new ManagementPermissionRepresentation(true)); + assertThat(realm.groups().group(id).getPermissions().isEnabled(), is(true)); + + return grp; + } +}