Skip to content

Commit

Permalink
[KEYCLOAK-2343] - Allow exact user search by user attributes
Browse files Browse the repository at this point in the history
Co-authored-by: Hynek Mlnařík <hmlnarik@users.noreply.github.com>
  • Loading branch information
pedroigor and hmlnarik committed Jun 10, 2020
1 parent 8142b9a commit e16f30d
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ List<UserRepresentation> search(@QueryParam("username") String username,
@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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,11 @@ public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel
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() + "%"));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ public List<UserRepresentation> getUsers(@QueryParam("search") String search,
@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();
Expand Down Expand Up @@ -237,6 +238,9 @@ public List<UserRepresentation> getUsers(@QueryParam("search") String search,
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,11 @@ public List<UserModel> searchForUser(Map<String, String> params, RealmModel real
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()));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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>
Expand Down Expand Up @@ -174,28 +175,18 @@ public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResult

@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
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
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
Expand All @@ -222,4 +213,21 @@ public List<UserModel> searchForUserByUserAttribute(String attrName, String attr
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,22 @@ public void searchByUsername() {
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -474,6 +475,13 @@ public void testQuery() {
});
}

@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);
Expand Down

0 comments on commit e16f30d

Please sign in to comment.