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.
This commit is contained in:
Thomas Darimont 2020-08-12 12:47:17 +02:00 committed by Pedro Igor
parent fbe18e67c3
commit 12576e339d
6 changed files with 124 additions and 3 deletions

View File

@ -53,6 +53,26 @@ public interface UsersResource {
@QueryParam("enabled") Boolean enabled,
@QueryParam("briefRepresentation") Boolean briefRepresentation);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> 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<UserRepresentation> 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<UserRepresentation> 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);

View File

@ -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())));
}

View File

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

View File

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

View File

@ -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<UserRepresentation> 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);

View File

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