diff --git a/docs/documentation/release_notes/topics/22_0_0.adoc b/docs/documentation/release_notes/topics/22_0_0.adoc index 0d10837926..ba1250e7e6 100644 --- a/docs/documentation/release_notes/topics/22_0_0.adoc +++ b/docs/documentation/release_notes/topics/22_0_0.adoc @@ -94,4 +94,16 @@ In version 21.1.0 of Keycloak the new Account Console (version 3) was introduced Two of the variables exposed to the Account Console V2 and V3 templates (`isEventsEnabled` and `isTotpConfigured`) were left unused, and have been removed in this release. -It is possible that if a developer extended the Account Console theme, he or she could make use of these variables. So make sure that these variables are no longer used if you are extending the base theme. \ No newline at end of file +It is possible that if a developer extended the Account Console theme, he or she could make use of these variables. So make sure that these variables are no longer used if you are extending the base theme. + += Support for count users based on custom attributes + +The User API now supports querying the number of users based on custom attributes. For that, a new `q` parameter was added to the `/{realm}/users/count` endpoint. + +The `q` parameter expects the following format: + +``` +q=: : ... +``` + +Where `` and `` represent the attribute name and value, respectively. diff --git a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/UsersResource.java b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/UsersResource.java index 481516293c..50aee2c381 100755 --- a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/UsersResource.java +++ b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/UsersResource.java @@ -308,7 +308,8 @@ public interface UsersResource { @QueryParam("email") String email, @QueryParam("emailVerified") Boolean emailVerified, @QueryParam("username") String username, - @QueryParam("enabled") Boolean enabled); + @QueryParam("enabled") Boolean enabled, + @QueryParam("q") String searchQuery); /** * Returns the number of users with the given status for emailVerified. diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java index 83cef98a55..35024ffe4e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java @@ -559,20 +559,7 @@ public class MapUserProvider implements UserProvider { return (int) storeWithRealm(realm).getCount(withCriteria(mcb)); } - @Override - public Stream searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { - LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace()); - Map attributes = new HashMap<>(); - attributes.put(UserModel.SEARCH, search); - attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, Boolean.FALSE.toString()); - return searchForUserStream(realm, attributes, firstResult, maxResults); - } - - @Override - public Stream searchForUserStream(RealmModel realm, Map attributes, Integer firstResult, Integer maxResults) { - LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, attributes, firstResult, maxResults, getShortStackTrace()); - - final DefaultModelCriteria mcb = criteria(); + private DefaultModelCriteria resolveCriteria(RealmModel realm, Map attributes, DefaultModelCriteria mcb) { DefaultModelCriteria criteria = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); final boolean exactSearch = Boolean.parseBoolean(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString())); @@ -649,6 +636,34 @@ public class MapUserProvider implements UserProvider { break; } } + return criteria; + } + + @Override + public int getUsersCount(RealmModel realm, Map attributes) { + LOG.tracef("getUsersCount(%s, %s)%s", realm, attributes, getShortStackTrace()); + + final DefaultModelCriteria mcb = criteria(); + DefaultModelCriteria criteria = resolveCriteria(realm, attributes, mcb); + + return (int) storeWithRealm(realm).getCount(withCriteria(criteria)); + } + + @Override + public Stream searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { + LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace()); + Map attributes = new HashMap<>(); + attributes.put(UserModel.SEARCH, search); + attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, Boolean.FALSE.toString()); + return searchForUserStream(realm, attributes, firstResult, maxResults); + } + + @Override + public Stream searchForUserStream(RealmModel realm, Map attributes, Integer firstResult, Integer maxResults) { + LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, attributes, firstResult, maxResults, getShortStackTrace()); + + final DefaultModelCriteria mcb = criteria(); + DefaultModelCriteria criteria = resolveCriteria(realm, attributes, mcb); // Only return those results that the current user is authorized to view, // i.e. there is an intersection of groups with view permission of the current 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 c6f43f0511..0b63486548 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 @@ -370,10 +370,15 @@ public class UsersResource { @QueryParam("email") String email, @QueryParam("emailVerified") Boolean emailVerified, @QueryParam("username") String username, - @QueryParam("enabled") Boolean enabled) { + @QueryParam("enabled") Boolean enabled, + @QueryParam("q") String searchQuery) { UserPermissionEvaluator userPermissionEvaluator = auth.users(); userPermissionEvaluator.requireQuery(); + Map searchAttributes = searchQuery == null + ? Collections.emptyMap() + : SearchQueryUtils.getFields(searchQuery); + if (search != null) { if (search.startsWith(SEARCH_ID_PARAMETER)) { UserModel userModel = session.users().getUserById(realm, search.substring(SEARCH_ID_PARAMETER.length()).trim()); @@ -383,7 +388,7 @@ public class UsersResource { } else { return session.users().getUsersCount(realm, search.trim(), auth.groups().getGroupsWithViewPermission()); } - } else if (last != null || first != null || email != null || username != null || emailVerified != null || enabled != null) { + } else if (last != null || first != null || email != null || username != null || emailVerified != null || enabled != null || !searchAttributes.isEmpty()) { Map parameters = new HashMap<>(); if (last != null) { parameters.put(UserModel.LAST_NAME, last); @@ -403,6 +408,8 @@ public class UsersResource { if (enabled != null) { parameters.put(UserModel.ENABLED, enabled.toString()); } + parameters.putAll(searchAttributes); + if (userPermissionEvaluator.canView()) { return session.users().getUsersCount(realm, parameters); } else { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 87e9112153..42cd8dbd25 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -640,6 +640,28 @@ public class UserTest extends AbstractAdminTest { return ids; } + @Test + public void countByAttribute() { + createUsers(); + + Map attributes = new HashMap<>(); + attributes.put("test1", "test2"); + assertThat(realm.users().count(null, null, null, null, null, null, null, mapToSearchQuery(attributes)), is(0)); + + attributes = new HashMap<>(); + attributes.put("test", "test1"); + assertThat(realm.users().count(null, null, null, null, null, null, null, mapToSearchQuery(attributes)), is(1)); + + attributes = new HashMap<>(); + attributes.put("test", "test2"); + attributes.put("attr", "common"); + assertThat(realm.users().count(null, null, null, null, null, null, null, mapToSearchQuery(attributes)), is(1)); + + attributes = new HashMap<>(); + attributes.put("attr", "common"); + assertThat(realm.users().count(null, null, null, null, null, null, null, mapToSearchQuery(attributes)), is(9)); + } + @Test public void countUsersByEnabledFilter() { @@ -666,16 +688,16 @@ public class UserTest extends AbstractAdminTest { Boolean disabled = false; // count all users with @enabledfilter.com - assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, null, null), is(3)); + assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, null, null, null), is(3)); // count users that are enabled and have username enabled1 - assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, "enabled1", enabled),is(1)); + assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, "enabled1", enabled, null),is(1)); // count users that are disabled - assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, null, disabled), is(1)); + assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, null, disabled, null), is(1)); // count users that are enabled - assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, null, enabled), is(2)); + assertThat(realm.users().count(null, null, null, "@enabledfilter.com", null, null, enabled, null), is(2)); } @Test