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");