[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:
parent
8142b9ad7f
commit
e16f30d31f
8 changed files with 67 additions and 18 deletions
|
@ -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:
|
||||
|
|
|
@ -868,9 +868,13 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
case UserModel.FIRST_NAME:
|
||||
case UserModel.LAST_NAME:
|
||||
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() + "%"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> userGroups = (Set<String>) session.getAttribute(UserModel.GROUPS);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -350,9 +350,13 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
|
|||
switch (key) {
|
||||
case UserModel.USERNAME:
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return userStream
|
||||
.skip(firstResult)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue