From b7eaa9b0cb8bd8e1216ecd27ca71390abbd4c9dc Mon Sep 17 00:00:00 2001 From: vramik Date: Wed, 2 Oct 2024 11:56:07 +0200 Subject: [PATCH] Wildcard search not working for custom user attributes Closes #32451 Signed-off-by: vramik --- .../topics/users/proc-searching-user.adoc | 23 +++++++++++++--- .../admin/client/resource/UsersResource.java | 6 +++++ .../keycloak/models/jpa/JpaUserProvider.java | 26 ++++++++++++------- .../keycloak/testsuite/admin/UserTest.java | 15 ++++++----- 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/docs/documentation/server_admin/topics/users/proc-searching-user.adoc b/docs/documentation/server_admin/topics/users/proc-searching-user.adoc index 5f102d742e..ecc6fdada9 100644 --- a/docs/documentation/server_admin/topics/users/proc-searching-user.adoc +++ b/docs/documentation/server_admin/topics/users/proc-searching-user.adoc @@ -10,6 +10,8 @@ Search for a user to view detailed information about the user, such as the user' .Prerequisite * You are in the realm where the user exists. +== Default search + .Procedure . Click *Users* in the main menu. This *Users* page is displayed. . Type the full name, last name, first name, or email address of the user you want to search for in the search box. The search returns all users that match your criteria. @@ -19,8 +21,21 @@ The criteria used to match users depends on the syntax used on the search box: .. `"somevalue"` -> performs exact search of the string `"somevalue"`; .. `\*somevalue*` -> performs infix search, akin to a `LIKE '%somevalue%'` DB query; .. `somevalue*` or `somevalue` -> performs prefix search, akin to a `LIKE 'somevalue%'` DB query. -+ -NOTE: Searches performed in the *Users* page encompasses searching both {project_name}'s database and configured user federated backends, such as LDAP. Users found in federated backends will be imported into {project_name}'s database if they don't already exist there. -+ -.Additional resources + +== Attribute search + +.Procedure +. Click *Users* in the main menu. This *Users* page is displayed. +. Click *Default search* button and switch it to *Attribute search*. +. Click *Select attributes* button and specify the attributes to search by. +. Check *Exact search* checkbox to perform exact match or keep it unchecked to use an infix search for attribute values. +. Click *Search* button to perform the search. It returns all users that match the criteria. + + +[NOTE] +==== +Searches performed in the *Users* page encompass both {project_name}'s database and configured user federation backends, such as LDAP. Users found in federated backends will be imported into {project_name}'s database if they don't already exist there. +==== + +.Additional Resources * For more information on user federation, see <<_user-storage-federation,User Federation>>. diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java index 7bb5382049..cf7d6c8ef4 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java @@ -114,6 +114,12 @@ public interface UsersResource { @Consumes(MediaType.APPLICATION_JSON) List searchByAttributes(@QueryParam("q") String searchQuery); + @GET + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + List searchByAttributes(@QueryParam("q") String searchQuery, + @QueryParam("exact") Boolean exact); + @GET @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 8e4608a23d..0627a17410 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -294,7 +294,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { return null; } - StorageId clientStorageId = null; + StorageId clientStorageId; if ( entity.getClientId() == null) { clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId()); } else { @@ -625,7 +625,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { predicates.add(builder.or(getSearchOptionPredicateArray(stringToSearch, builder, root))); } - queryBuilder.where(predicates.toArray(new Predicate[0])); + queryBuilder.where(predicates.toArray(Predicate[]::new)); return em.createQuery(queryBuilder).getSingleResult().intValue(); } @@ -654,7 +654,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { predicates.add(groupMembership.get("groupId").in(groupIds)); - queryBuilder.where(predicates.toArray(new Predicate[0])); + queryBuilder.where(predicates.toArray(Predicate[]::new)); return em.createQuery(queryBuilder).getSingleResult().intValue(); } @@ -670,7 +670,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { List restrictions = predicates(params, from, Map.of()); restrictions.add(qb.equal(from.get("realmId"), realm.getId())); - userQuery = userQuery.where(restrictions.toArray(new Predicate[0])); + userQuery = userQuery.where(restrictions.toArray(Predicate[]::new)); TypedQuery query = em.createQuery(userQuery); Long result = query.getSingleResult(); @@ -695,7 +695,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { groupsWithPermissionsSubquery(countQuery, groupIds, root, restrictions); - countQuery.where(restrictions.toArray(new Predicate[0])); + countQuery.where(restrictions.toArray(Predicate[]::new)); TypedQuery query = em.createQuery(countQuery); Long result = query.getSingleResult(); @@ -952,7 +952,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value, ESCAPE_BACKSLASH)); } - return orPredicates.toArray(new Predicate[0]); + return orPredicates.toArray(Predicate[]::new); } private UserEntity userInEntityManagerContext(String id) { @@ -1027,9 +1027,15 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { builder.equal(attributesJoin.get("name"), key), builder.equal(attributesJoin.get("longValueHashLowerCase"), JpaHashUtils.hashForAttributeValueLowerCase(value)))); } else { - attributePredicates.add(builder.and( + if (Boolean.parseBoolean(attributes.get(UserModel.EXACT))) { + attributePredicates.add(builder.and( builder.equal(attributesJoin.get("name"), key), builder.equal(builder.lower(attributesJoin.get("value")), value.toLowerCase()))); + } else { + attributePredicates.add(builder.and( + builder.equal(attributesJoin.get("name"), key), + builder.like(builder.lower(attributesJoin.get("value")), "%" + value.toLowerCase() + "%"))); + } } break; case UserModel.INCLUDE_SERVICE_ACCOUNT: { @@ -1043,7 +1049,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { } if (!attributePredicates.isEmpty()) { - predicates.add(builder.and(attributePredicates.toArray(new Predicate[0]))); + predicates.add(builder.and(attributePredicates.toArray(Predicate[]::new))); } return predicates; @@ -1074,11 +1080,11 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { Expression groupId = from.get("groupId"); subs.add(cb.like(from1.get("name"), cb.concat("group.resource.", groupId))); - subquery1.where(subs.toArray(new Predicate[0])); + subquery1.where(subs.toArray(Predicate[]::new)); subPredicates.add(cb.exists(subquery1)); - subquery.where(subPredicates.toArray(new Predicate[0])); + subquery.where(subPredicates.toArray(Predicate[]::new)); restrictions.add(cb.exists(subquery)); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 48b43ca61e..3d3c05408b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -918,13 +918,16 @@ public class UserTest extends AbstractAdminTest { public void searchByMultipleAttributes() { createUsers(); - Map attributes = new HashMap<>(); - attributes.put("test", "test1"); - attributes.put("attr", "common"); - attributes.put("test1", "test1"); + List users = realm.users().searchByAttributes(mapToSearchQuery(Map.of("username", "user", "test", "test1", "attr", "common", "test1", "test1"))); + assertThat(users, hasSize(1)); - List users = realm.users().searchByAttributes(mapToSearchQuery(attributes)); - assertEquals(1, users.size()); + //custom user attribute should use wildcard search by default + users = realm.users().searchByAttributes(mapToSearchQuery(Map.of("username", "user", "test", "est", "attr", "mm", "test1", "test1"))); + assertThat(users, hasSize(1)); + + //with exact=true the user shouldn't be returned + users = realm.users().searchByAttributes(mapToSearchQuery(Map.of("test", "est", "attr", "mm", "test1", "test1")), Boolean.TRUE); + assertThat(users, hasSize(0)); } @Test