[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)
|
@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:
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue