From 12576e339d516b38b947e2eb9fa93193942178fe Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Wed, 12 Aug 2020 12:47:17 +0200 Subject: [PATCH] KEYCLOAK-15146 Add support for searching users by emailVerified status We now allow to search for users by their emailVerified status. This enables users to easily find users and deal with incomplete user accounts. --- .../admin/client/resource/UsersResource.java | 52 +++++++++++++++++++ .../keycloak/models/jpa/JpaUserProvider.java | 10 ++++ .../java/org/keycloak/models/UserModel.java | 1 + .../resources/admin/UsersResource.java | 14 +++-- .../testsuite/AbstractKeycloakTest.java | 7 +++ .../keycloak/testsuite/admin/UsersTest.java | 43 +++++++++++++++ 6 files changed, 124 insertions(+), 3 deletions(-) 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 88fe5ec806..ebd211413d 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 @@ -53,6 +53,26 @@ public interface UsersResource { @QueryParam("enabled") Boolean enabled, @QueryParam("briefRepresentation") Boolean briefRepresentation); + @GET + @Produces(MediaType.APPLICATION_JSON) + List search(@QueryParam("username") String username, + @QueryParam("firstName") String firstName, + @QueryParam("lastName") String lastName, + @QueryParam("email") String email, + @QueryParam("emailVerified") Boolean emailVerified, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, + @QueryParam("enabled") Boolean enabled, + @QueryParam("briefRepresentation") Boolean briefRepresentation); + + @GET + @Produces(MediaType.APPLICATION_JSON) + List search(@QueryParam("emailVerified") Boolean emailVerified, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, + @QueryParam("enabled") Boolean enabled, + @QueryParam("briefRepresentation") Boolean briefRepresentation); + @GET @Produces(MediaType.APPLICATION_JSON) List search(@QueryParam("username") String username); @@ -156,6 +176,38 @@ public interface UsersResource { @QueryParam("email") String email, @QueryParam("username") String username); + /** + * 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 emailVerified emailVerified 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("emailVerified") Boolean emailVerified, + @QueryParam("username") String username); + + /** + * Returns the number of users with the given status for emailVerified. + * If none of the filters is specified this is equivalent to {{@link #count()}}. + * + * @param emailVerified emailVerified field of a user + * @return number of users matching the given filters + */ + @Path("count") + @GET + @Produces(MediaType.APPLICATION_JSON) + Integer countEmailVerified(@QueryParam("emailVerified") Boolean emailVerified); + @Path("{id}") UserResource get(@PathParam("id") String id); 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 206f567fad..d5f4daa8ec 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 @@ -77,6 +77,7 @@ import javax.persistence.LockModeType; public class JpaUserProvider implements UserProvider, UserCredentialStore { private static final String EMAIL = "email"; + private static final String EMAIL_VERIFIED = "emailVerified"; private static final String USERNAME = "username"; private static final String FIRST_NAME = "firstName"; private static final String LAST_NAME = "lastName"; @@ -685,6 +686,9 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { case UserModel.EMAIL: restrictions.add(qb.like(from.get("email"), "%" + value + "%")); break; + case UserModel.EMAIL_VERIFIED: + restrictions.add(qb.equal(from.get("emailVerified"), Boolean.parseBoolean(value.toLowerCase()))); + break; } } @@ -731,6 +735,9 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { case UserModel.EMAIL: restrictions.add(qb.like(from.get("user").get("email"), "%" + value + "%")); break; + case UserModel.EMAIL_VERIFIED: + restrictions.add(qb.equal(from.get("emailVerified"), Boolean.parseBoolean(value.toLowerCase()))); + break; } } @@ -873,6 +880,9 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { predicates.add(builder.like(builder.lower(root.get(key)), "%" + value.toLowerCase() + "%")); } break; + case EMAIL_VERIFIED: + predicates.add(builder.equal(root.get(key), Boolean.parseBoolean(value.toLowerCase()))); + break; case UserModel.ENABLED: predicates.add(builder.equal(builder.lower(root.get(key)), Boolean.parseBoolean(value.toLowerCase()))); } 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 2cc40eb4e5..0782a88a7d 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -35,6 +35,7 @@ public interface UserModel extends RoleMapperModel { String FIRST_NAME = "firstName"; String LAST_NAME = "lastName"; String EMAIL = "email"; + String EMAIL_VERIFIED = "emailVerified"; String LOCALE = "locale"; String ENABLED = "enabled"; String INCLUDE_SERVICE_ACCOUNT = "keycloak.session.realm.users.query.include_service_account"; 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 955a8ba7ad..076d1ec94c 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 @@ -231,6 +231,7 @@ public class UsersResource { @QueryParam("firstName") String first, @QueryParam("email") String email, @QueryParam("username") String username, + @QueryParam("emailVerified") Boolean emailVerified, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults, @QueryParam("enabled") Boolean enabled, @@ -248,7 +249,7 @@ public class UsersResource { if (search.startsWith(SEARCH_ID_PARAMETER)) { UserModel userModel = session.users().getUserById(search.substring(SEARCH_ID_PARAMETER.length()).trim(), realm); if (userModel != null) { - userModels = Arrays.asList(userModel); + userModels = Collections.singletonList(userModel); } } else { Map attributes = new HashMap<>(); @@ -258,7 +259,7 @@ public class UsersResource { } return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false); } - } else if (last != null || first != null || email != null || username != null || enabled != null || exact != null) { + } else if (last != null || first != null || email != null || username != null || emailVerified != null || enabled != null || exact != null) { Map attributes = new HashMap<>(); if (last != null) { attributes.put(UserModel.LAST_NAME, last); @@ -278,6 +279,9 @@ public class UsersResource { if (exact != null) { attributes.put(UserModel.EXACT, exact.toString()); } + if (emailVerified != null) { + attributes.put(UserModel.EMAIL_VERIFIED, emailVerified.toString()); + } return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, true); } else { return searchForUser(new HashMap<>(), realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false); @@ -316,6 +320,7 @@ public class UsersResource { @QueryParam("lastName") String last, @QueryParam("firstName") String first, @QueryParam("email") String email, + @QueryParam("emailVerified") Boolean emailVerified, @QueryParam("username") String username) { UserPermissionEvaluator userPermissionEvaluator = auth.users(); userPermissionEvaluator.requireQuery(); @@ -329,7 +334,7 @@ public class UsersResource { } else { return session.users().getUsersCount(search.trim(), realm, auth.groups().getGroupsWithViewPermission()); } - } else if (last != null || first != null || email != null || username != null) { + } else if (last != null || first != null || email != null || username != null || emailVerified != null) { Map parameters = new HashMap<>(); if (last != null) { parameters.put(UserModel.LAST_NAME, last); @@ -343,6 +348,9 @@ public class UsersResource { if (username != null) { parameters.put(UserModel.USERNAME, username); } + if (emailVerified != null) { + parameters.put(UserModel.EMAIL_VERIFIED, emailVerified.toString()); + } if (userPermissionEvaluator.canView()) { return session.users().getUsersCount(parameters, realm); } else { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java index f8c8086e48..85d36df296 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java @@ -76,6 +76,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Scanner; +import java.util.function.Consumer; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -492,6 +493,12 @@ public abstract class AbstractKeycloakTest { return ApiUtil.createUserWithAdminClient(adminClient.realm(realm), homer); } + public String createUser(String realm, String username, String password, String firstName, String lastName, String email, Consumer customizer) { + UserRepresentation user = createUserRepresentation(username, email, firstName, lastName, true, password); + customizer.accept(user); + return ApiUtil.createUserWithAdminClient(adminClient.realm(realm), user); + } + public String createUser(String realm, String username, String password, String firstName, String lastName, String email) { UserRepresentation homer = createUserRepresentation(username, email, firstName, lastName, true, password); return ApiUtil.createUserWithAdminClient(adminClient.realm(realm), homer); 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 index b2f629359d..1770fb4ecf 100644 --- 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 @@ -44,6 +44,8 @@ import java.util.List; import java.util.Optional; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; public class UsersTest extends AbstractAdminTest { @@ -56,6 +58,47 @@ public class UsersTest extends AbstractAdminTest { } } + /** + * https://issues.redhat.com/browse/KEYCLOAK-15146 + */ + @Test + public void findUsersByEmailVerifiedStatus() { + + createUser(realmId, "user1", "password", "user1FirstName", "user1LastName", "user1@example.com", rep -> rep.setEmailVerified(true)); + createUser(realmId, "user2", "password", "user2FirstName", "user2LastName", "user2@example.com", rep -> rep.setEmailVerified(false)); + + boolean emailVerified; + emailVerified = true; + List usersEmailVerified = realm.users().search(null, null, null, null, emailVerified, null, null, null, true); + assertThat(usersEmailVerified, is(not(empty()))); + assertThat(usersEmailVerified.get(0).getUsername(), is("user1")); + + emailVerified = false; + List usersEmailNotVerified = realm.users().search(null, null, null, null, emailVerified, null, null, null, true); + assertThat(usersEmailNotVerified, is(not(empty()))); + assertThat(usersEmailNotVerified.get(0).getUsername(), is("user2")); + } + + /** + * https://issues.redhat.com/browse/KEYCLOAK-15146 + */ + @Test + public void countUsersByEmailVerifiedStatus() { + + createUser(realmId, "user1", "password", "user1FirstName", "user1LastName", "user1@example.com", rep -> rep.setEmailVerified(true)); + createUser(realmId, "user2", "password", "user2FirstName", "user2LastName", "user2@example.com", rep -> rep.setEmailVerified(false)); + createUser(realmId, "user3", "password", "user3FirstName", "user3LastName", "user3@example.com", rep -> rep.setEmailVerified(true)); + + boolean emailVerified; + emailVerified = true; + assertThat(realm.users().countEmailVerified(emailVerified), is(2)); + assertThat(realm.users().count(null,null,null,emailVerified,null), is(2)); + + emailVerified = false; + assertThat(realm.users().countEmailVerified(emailVerified), is(1)); + assertThat(realm.users().count(null,null,null,emailVerified,null), is(1)); + } + @Test public void countUsersWithViewPermission() { createUser(realmId, "user1", "password", "user1FirstName", "user1LastName", "user1@example.com");