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

@ -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. 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. 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("email") String email,
@QueryParam("emailVerified") Boolean emailVerified, @QueryParam("emailVerified") Boolean emailVerified,
@QueryParam("username") String username, @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. * 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)); return (int) storeWithRealm(realm).getCount(withCriteria(mcb));
} }
@Override private DefaultModelCriteria<UserModel> resolveCriteria(RealmModel realm, Map<String, String> attributes, DefaultModelCriteria<UserModel> mcb) {
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 = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); DefaultModelCriteria<UserModel> criteria = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
final boolean exactSearch = Boolean.parseBoolean(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString())); final boolean exactSearch = Boolean.parseBoolean(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()));
@ -649,6 +636,34 @@ public class MapUserProvider implements UserProvider {
break; 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, // 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 // 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("email") String email,
@QueryParam("emailVerified") Boolean emailVerified, @QueryParam("emailVerified") Boolean emailVerified,
@QueryParam("username") String username, @QueryParam("username") String username,
@QueryParam("enabled") Boolean enabled) { @QueryParam("enabled") Boolean enabled,
@QueryParam("q") String searchQuery) {
UserPermissionEvaluator userPermissionEvaluator = auth.users(); UserPermissionEvaluator userPermissionEvaluator = auth.users();
userPermissionEvaluator.requireQuery(); userPermissionEvaluator.requireQuery();
Map<String, String> searchAttributes = searchQuery == null
? Collections.emptyMap()
: SearchQueryUtils.getFields(searchQuery);
if (search != null) { if (search != null) {
if (search.startsWith(SEARCH_ID_PARAMETER)) { if (search.startsWith(SEARCH_ID_PARAMETER)) {
UserModel userModel = session.users().getUserById(realm, search.substring(SEARCH_ID_PARAMETER.length()).trim()); UserModel userModel = session.users().getUserById(realm, search.substring(SEARCH_ID_PARAMETER.length()).trim());
@ -383,7 +388,7 @@ public class UsersResource {
} else { } else {
return session.users().getUsersCount(realm, search.trim(), auth.groups().getGroupsWithViewPermission()); 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<>(); Map<String, String> parameters = new HashMap<>();
if (last != null) { if (last != null) {
parameters.put(UserModel.LAST_NAME, last); parameters.put(UserModel.LAST_NAME, last);
@ -403,6 +408,8 @@ public class UsersResource {
if (enabled != null) { if (enabled != null) {
parameters.put(UserModel.ENABLED, enabled.toString()); parameters.put(UserModel.ENABLED, enabled.toString());
} }
parameters.putAll(searchAttributes);
if (userPermissionEvaluator.canView()) { if (userPermissionEvaluator.canView()) {
return session.users().getUsersCount(realm, parameters); return session.users().getUsersCount(realm, parameters);
} else { } else {

View file

@ -640,6 +640,28 @@ public class UserTest extends AbstractAdminTest {
return ids; 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 @Test
public void countUsersByEnabledFilter() { public void countUsersByEnabledFilter() {
@ -666,16 +688,16 @@ public class UserTest extends AbstractAdminTest {
Boolean disabled = false; Boolean disabled = false;
// count all users with @enabledfilter.com // 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 // 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 // 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 // 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 @Test