Skip to content

Commit b7eaa9b

Browse files
vramikpedroigor
authored andcommitted
Wildcard search not working for custom user attributes
Closes #32451 Signed-off-by: vramik <[email protected]>
1 parent 35eba8b commit b7eaa9b

File tree

4 files changed

+50
-20
lines changed

4 files changed

+50
-20
lines changed

docs/documentation/server_admin/topics/users/proc-searching-user.adoc

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Search for a user to view detailed information about the user, such as the user'
1010
.Prerequisite
1111
* You are in the realm where the user exists.
1212

13+
== Default search
14+
1315
.Procedure
1416
. Click *Users* in the main menu. This *Users* page is displayed.
1517
. 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:
1921
.. `"somevalue"` -> performs exact search of the string `"somevalue"`;
2022
.. `\*somevalue*` -> performs infix search, akin to a `LIKE '%somevalue%'` DB query;
2123
.. `somevalue*` or `somevalue` -> performs prefix search, akin to a `LIKE 'somevalue%'` DB query.
22-
+
23-
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.
24-
+
25-
.Additional resources
24+
25+
== Attribute search
26+
27+
.Procedure
28+
. Click *Users* in the main menu. This *Users* page is displayed.
29+
. Click *Default search* button and switch it to *Attribute search*.
30+
. Click *Select attributes* button and specify the attributes to search by.
31+
. Check *Exact search* checkbox to perform exact match or keep it unchecked to use an infix search for attribute values.
32+
. Click *Search* button to perform the search. It returns all users that match the criteria.
33+
34+
35+
[NOTE]
36+
====
37+
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.
38+
====
39+
40+
.Additional Resources
2641
* For more information on user federation, see <<_user-storage-federation,User Federation>>.

integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ List<UserRepresentation> search(@QueryParam("emailVerified") Boolean emailVerifi
114114
@Consumes(MediaType.APPLICATION_JSON)
115115
List<UserRepresentation> searchByAttributes(@QueryParam("q") String searchQuery);
116116

117+
@GET
118+
@Produces(MediaType.APPLICATION_JSON)
119+
@Consumes(MediaType.APPLICATION_JSON)
120+
List<UserRepresentation> searchByAttributes(@QueryParam("q") String searchQuery,
121+
@QueryParam("exact") Boolean exact);
122+
117123
@GET
118124
@Produces(MediaType.APPLICATION_JSON)
119125
@Consumes(MediaType.APPLICATION_JSON)

model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ private UserConsentModel toConsentModel(RealmModel realm, UserConsentEntity enti
294294
return null;
295295
}
296296

297-
StorageId clientStorageId = null;
297+
StorageId clientStorageId;
298298
if ( entity.getClientId() == null) {
299299
clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId());
300300
} else {
@@ -625,7 +625,7 @@ public int getUsersCount(RealmModel realm, String search) {
625625
predicates.add(builder.or(getSearchOptionPredicateArray(stringToSearch, builder, root)));
626626
}
627627

628-
queryBuilder.where(predicates.toArray(new Predicate[0]));
628+
queryBuilder.where(predicates.toArray(Predicate[]::new));
629629

630630
return em.createQuery(queryBuilder).getSingleResult().intValue();
631631
}
@@ -654,7 +654,7 @@ public int getUsersCount(RealmModel realm, String search, Set<String> groupIds)
654654

655655
predicates.add(groupMembership.get("groupId").in(groupIds));
656656

657-
queryBuilder.where(predicates.toArray(new Predicate[0]));
657+
queryBuilder.where(predicates.toArray(Predicate[]::new));
658658

659659
return em.createQuery(queryBuilder).getSingleResult().intValue();
660660
}
@@ -670,7 +670,7 @@ public int getUsersCount(RealmModel realm, Map<String, String> params) {
670670
List<Predicate> restrictions = predicates(params, from, Map.of());
671671
restrictions.add(qb.equal(from.get("realmId"), realm.getId()));
672672

673-
userQuery = userQuery.where(restrictions.toArray(new Predicate[0]));
673+
userQuery = userQuery.where(restrictions.toArray(Predicate[]::new));
674674
TypedQuery<Long> query = em.createQuery(userQuery);
675675
Long result = query.getSingleResult();
676676

@@ -695,7 +695,7 @@ public int getUsersCount(RealmModel realm, Map<String, String> params, Set<Strin
695695

696696
groupsWithPermissionsSubquery(countQuery, groupIds, root, restrictions);
697697

698-
countQuery.where(restrictions.toArray(new Predicate[0]));
698+
countQuery.where(restrictions.toArray(Predicate[]::new));
699699
TypedQuery<Long> query = em.createQuery(countQuery);
700700
Long result = query.getSingleResult();
701701

@@ -952,7 +952,7 @@ private Predicate[] getSearchOptionPredicateArray(String value, CriteriaBuilder
952952
orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value, ESCAPE_BACKSLASH));
953953
}
954954

955-
return orPredicates.toArray(new Predicate[0]);
955+
return orPredicates.toArray(Predicate[]::new);
956956
}
957957

958958
private UserEntity userInEntityManagerContext(String id) {
@@ -1027,9 +1027,15 @@ private List<Predicate> predicates(Map<String, String> attributes, Root<UserEnti
10271027
builder.equal(attributesJoin.get("name"), key),
10281028
builder.equal(attributesJoin.get("longValueHashLowerCase"), JpaHashUtils.hashForAttributeValueLowerCase(value))));
10291029
} else {
1030-
attributePredicates.add(builder.and(
1030+
if (Boolean.parseBoolean(attributes.get(UserModel.EXACT))) {
1031+
attributePredicates.add(builder.and(
10311032
builder.equal(attributesJoin.get("name"), key),
10321033
builder.equal(builder.lower(attributesJoin.get("value")), value.toLowerCase())));
1034+
} else {
1035+
attributePredicates.add(builder.and(
1036+
builder.equal(attributesJoin.get("name"), key),
1037+
builder.like(builder.lower(attributesJoin.get("value")), "%" + value.toLowerCase() + "%")));
1038+
}
10331039
}
10341040
break;
10351041
case UserModel.INCLUDE_SERVICE_ACCOUNT: {
@@ -1043,7 +1049,7 @@ private List<Predicate> predicates(Map<String, String> attributes, Root<UserEnti
10431049
}
10441050

10451051
if (!attributePredicates.isEmpty()) {
1046-
predicates.add(builder.and(attributePredicates.toArray(new Predicate[0])));
1052+
predicates.add(builder.and(attributePredicates.toArray(Predicate[]::new)));
10471053
}
10481054

10491055
return predicates;
@@ -1074,11 +1080,11 @@ private void groupsWithPermissionsSubquery(CriteriaQuery<?> query, Set<String> g
10741080
Expression<String> groupId = from.get("groupId");
10751081
subs.add(cb.like(from1.get("name"), cb.concat("group.resource.", groupId)));
10761082

1077-
subquery1.where(subs.toArray(new Predicate[0]));
1083+
subquery1.where(subs.toArray(Predicate[]::new));
10781084

10791085
subPredicates.add(cb.exists(subquery1));
10801086

1081-
subquery.where(subPredicates.toArray(new Predicate[0]));
1087+
subquery.where(subPredicates.toArray(Predicate[]::new));
10821088

10831089
restrictions.add(cb.exists(subquery));
10841090
}

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -918,13 +918,16 @@ public void searchByAttribute() {
918918
public void searchByMultipleAttributes() {
919919
createUsers();
920920

921-
Map<String, String> attributes = new HashMap<>();
922-
attributes.put("test", "test1");
923-
attributes.put("attr", "common");
924-
attributes.put("test1", "test1");
921+
List<UserRepresentation> users = realm.users().searchByAttributes(mapToSearchQuery(Map.of("username", "user", "test", "test1", "attr", "common", "test1", "test1")));
922+
assertThat(users, hasSize(1));
925923

926-
List<UserRepresentation> users = realm.users().searchByAttributes(mapToSearchQuery(attributes));
927-
assertEquals(1, users.size());
924+
//custom user attribute should use wildcard search by default
925+
users = realm.users().searchByAttributes(mapToSearchQuery(Map.of("username", "user", "test", "est", "attr", "mm", "test1", "test1")));
926+
assertThat(users, hasSize(1));
927+
928+
//with exact=true the user shouldn't be returned
929+
users = realm.users().searchByAttributes(mapToSearchQuery(Map.of("test", "est", "attr", "mm", "test1", "test1")), Boolean.TRUE);
930+
assertThat(users, hasSize(0));
928931
}
929932

930933
@Test

0 commit comments

Comments
 (0)