[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) @Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> search(@QueryParam("username") String username); 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} * 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: * argument also allows finding users by specific attributes as follows:

View file

@ -868,9 +868,13 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
case UserModel.FIRST_NAME: case UserModel.FIRST_NAME:
case UserModel.LAST_NAME: case UserModel.LAST_NAME:
case UserModel.EMAIL: case UserModel.EMAIL:
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() + "%")); predicates.add(builder.like(builder.lower(root.get(key)), "%" + value.toLowerCase() + "%"));
} }
} }
}
Set<String> userGroups = (Set<String>) session.getAttribute(UserModel.GROUPS); Set<String> userGroups = (Set<String>) session.getAttribute(UserModel.GROUPS);

View file

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

View file

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

View file

@ -350,9 +350,13 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
switch (key) { switch (key) {
case UserModel.USERNAME: case UserModel.USERNAME:
case UserModel.SEARCH: case UserModel.SEARCH:
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())); userStream = userStream.filter(s -> s.toLowerCase().contains(value.toLowerCase()));
} }
} }
}
return userStream return userStream
.skip(firstResult) .skip(firstResult)

View file

@ -39,6 +39,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import java.util.function.Predicate;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -174,20 +175,7 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
@Override @Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
if (maxResults == 0) return Collections.EMPTY_LIST; return searchForUser(search, realm, firstResult, maxResults, username -> username.contains(search));
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;
} }
@Override @Override
@ -195,7 +183,10 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
String search = Optional.ofNullable(attributes.get(UserModel.USERNAME)) String search = Optional.ofNullable(attributes.get(UserModel.USERNAME))
.orElseGet(()-> attributes.get(UserModel.SEARCH)); .orElseGet(()-> attributes.get(UserModel.SEARCH));
if (search == null) return Collections.EMPTY_LIST; 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 @Override
@ -222,4 +213,21 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
public void close() { 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()); 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 @Test
public void searchByFirstNameNullForLastName() { public void searchByFirstNameNullForLastName() {
UserRepresentation user = new UserRepresentation(); UserRepresentation user = new UserRepresentation();

View file

@ -1,6 +1,7 @@
package org.keycloak.testsuite.federation.storage; package org.keycloak.testsuite.federation.storage;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.After; import org.junit.After;
import org.junit.Assert; 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) { private void setDailyEvictionTime(int hour, int minutes) {
if (hour < 0 || hour > 23) { if (hour < 0 || hour > 23) {
throw new IllegalArgumentException("hour == " + hour); throw new IllegalArgumentException("hour == " + hour);