[KEYCLOAK-2343] - Allow exact user search by user attributes

Co-authored-by: Hynek Mlnařík <hmlnarik@users.noreply.github.com>
This commit is contained in:
Pedro Igor 2020-03-24 15:05:04 -03:00
parent 8142b9ad7f
commit e16f30d31f
8 changed files with 67 additions and 18 deletions

View file

@ -56,6 +56,10 @@ public interface UsersResource {
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> search(@QueryParam("username") String username);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> search(@QueryParam("username") String username, @QueryParam("exact") Boolean exact);
/**
* Search for users whose username or email matches the value provided by {@code search}. The {@code search}
* argument also allows finding users by specific attributes as follows:

View file

@ -868,7 +868,11 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
case UserModel.FIRST_NAME:
case UserModel.LAST_NAME:
case UserModel.EMAIL:
predicates.add(builder.like(builder.lower(root.get(key)), "%" + value.toLowerCase() + "%"));
if (Boolean.valueOf(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()))) {
predicates.add(builder.equal(builder.lower(root.get(key)), value.toLowerCase()));
} else {
predicates.add(builder.like(builder.lower(root.get(key)), "%" + value.toLowerCase() + "%"));
}
}
}

View file

@ -38,6 +38,7 @@ public interface UserModel extends RoleMapperModel {
String INCLUDE_SERVICE_ACCOUNT = "keycloak.session.realm.users.query.include_service_account";
String GROUPS = "keycloak.session.realm.users.query.groups";
String SEARCH = "keycloak.session.realm.users.query.search";
String EXACT = "keycloak.session.realm.users.query.exact";
interface UserRemovedEvent extends ProviderEvent {
RealmModel getRealm();

View file

@ -203,7 +203,8 @@ public class UsersResource {
@QueryParam("username") String username,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") Boolean briefRepresentation) {
@QueryParam("briefRepresentation") Boolean briefRepresentation,
@QueryParam("exact") Boolean exact) {
UserPermissionEvaluator userPermissionEvaluator = auth.users();
userPermissionEvaluator.requireQuery();
@ -237,6 +238,9 @@ public class UsersResource {
if (username != null) {
attributes.put(UserModel.USERNAME, username);
}
if (exact != null) {
attributes.put(UserModel.EXACT, exact.toString());
}
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, true);
} else {
return searchForUser(new HashMap<>(), realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false);

View file

@ -350,7 +350,11 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
switch (key) {
case UserModel.USERNAME:
case UserModel.SEARCH:
userStream = userStream.filter(s -> s.toLowerCase().contains(value.toLowerCase()));
if (Boolean.valueOf(params.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()))) {
userStream = userStream.filter(s -> s.toLowerCase().equals(value.toLowerCase()));
} else {
userStream = userStream.filter(s -> s.toLowerCase().contains(value.toLowerCase()));
}
}
}

View file

@ -39,6 +39,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Predicate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -174,20 +175,7 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
if (maxResults == 0) return Collections.EMPTY_LIST;
List<UserModel> users = new LinkedList<>();
int count = 0;
for (Object un : userPasswords.keySet()) {
String username = (String)un;
if (username.contains(search)) {
if (count++ < firstResult) {
continue;
}
users.add(createUser(realm, username));
if (users.size() + 1 > maxResults) break;
}
}
return users;
return searchForUser(search, realm, firstResult, maxResults, username -> username.contains(search));
}
@Override
@ -195,7 +183,10 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
String search = Optional.ofNullable(attributes.get(UserModel.USERNAME))
.orElseGet(()-> attributes.get(UserModel.SEARCH));
if (search == null) return Collections.EMPTY_LIST;
return searchForUser(search, realm, firstResult, maxResults);
Predicate<String> p = Boolean.valueOf(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()))
? username -> username.equals(search)
: username -> username.contains(search);
return searchForUser(search, realm, firstResult, maxResults, p);
}
@Override
@ -222,4 +213,21 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
public void close() {
}
private List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults, Predicate<String> matcher) {
if (maxResults == 0) return Collections.EMPTY_LIST;
List<UserModel> users = new LinkedList<>();
int count = 0;
for (Object un : userPasswords.keySet()) {
String username = (String)un;
if (matcher.test(username)) {
if (count++ < firstResult) {
continue;
}
users.add(createUser(realm, username));
if (users.size() + 1 > maxResults) break;
}
}
return users;
}
}

View file

@ -582,6 +582,22 @@ public class UserTest extends AbstractAdminTest {
assertEquals(9, users.size());
}
@Test
public void searchByUsernameExactMatch() {
createUsers();
UserRepresentation user = new UserRepresentation();
user.setUsername("username11");
createUser(user);
List<UserRepresentation> users = realm.users().search("username1", true);
assertEquals(1, users.size());
users = realm.users().search("user", true);
assertEquals(0, users.size());
}
@Test
public void searchByFirstNameNullForLastName() {
UserRepresentation user = new UserRepresentation();

View file

@ -1,6 +1,7 @@
package org.keycloak.testsuite.federation.storage;
import org.apache.commons.io.FileUtils;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Assert;
@ -474,6 +475,13 @@ public class UserStorageTest extends AbstractAuthTest {
});
}
@Test
public void testQueryExactMatch() {
Assert.assertThat(testRealmResource().users().search("a", true), Matchers.hasSize(0));
Assert.assertThat(testRealmResource().users().search("apollo", true), Matchers.hasSize(1));
Assert.assertThat(testRealmResource().users().search("tbrady", true), Matchers.hasSize(1));
}
private void setDailyEvictionTime(int hour, int minutes) {
if (hour < 0 || hour > 23) {
throw new IllegalArgumentException("hour == " + hour);