count users by custom user attribute

closes #14747
This commit is contained in:
Gilvan Filho 2022-10-14 10:38:03 -03:00 committed by Pedro Igor
parent dc3b037e3a
commit 2493f11331
5 changed files with 79 additions and 22 deletions

View file

@ -95,3 +95,15 @@ 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.
= 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=<name>:<value> <name>:<value> ...
```
Where `<name>` and `<value>` represent the attribute name and value, respectively.

View file

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

View file

@ -559,20 +559,7 @@ public class MapUserProvider implements UserProvider {
return (int) storeWithRealm(realm).getCount(withCriteria(mcb));
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace());
Map<String, String> 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<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, attributes, firstResult, maxResults, getShortStackTrace());
final DefaultModelCriteria<UserModel> mcb = criteria();
private DefaultModelCriteria<UserModel> resolveCriteria(RealmModel realm, Map<String, String> attributes, DefaultModelCriteria<UserModel> mcb) {
DefaultModelCriteria<UserModel> 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<String, String> attributes) {
LOG.tracef("getUsersCount(%s, %s)%s", realm, attributes, getShortStackTrace());
final DefaultModelCriteria<UserModel> mcb = criteria();
DefaultModelCriteria<UserModel> criteria = resolveCriteria(realm, attributes, mcb);
return (int) storeWithRealm(realm).getCount(withCriteria(criteria));
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace());
Map<String, String> 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<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, attributes, firstResult, maxResults, getShortStackTrace());
final DefaultModelCriteria<UserModel> mcb = criteria();
DefaultModelCriteria<UserModel> 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

View file

@ -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<String, String> 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<String, String> 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 {

View file

@ -640,6 +640,28 @@ public class UserTest extends AbstractAdminTest {
return ids;
}
@Test
public void countByAttribute() {
createUsers();
Map<String, String> 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