Wildcard search not working for custom user attributes

Closes #32451

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2024-10-02 11:56:07 +02:00 committed by Pedro Igor
parent 35eba8be8c
commit b7eaa9b0cb
4 changed files with 50 additions and 20 deletions

View file

@ -10,6 +10,8 @@ Search for a user to view detailed information about the user, such as the user'
.Prerequisite .Prerequisite
* You are in the realm where the user exists. * You are in the realm where the user exists.
== Default search
.Procedure .Procedure
. Click *Users* in the main menu. This *Users* page is displayed. . 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. . 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 exact search of the string `"somevalue"`;
.. `\*somevalue*` -> performs infix search, akin to a `LIKE '%somevalue%'` DB query; .. `\*somevalue*` -> performs infix search, akin to a `LIKE '%somevalue%'` DB query;
.. `somevalue*` or `somevalue` -> performs prefix 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. == Attribute search
+
.Additional resources .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>>. * For more information on user federation, see <<_user-storage-federation,User Federation>>.

View file

@ -114,6 +114,12 @@ public interface UsersResource {
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
List<UserRepresentation> searchByAttributes(@QueryParam("q") String searchQuery); List<UserRepresentation> searchByAttributes(@QueryParam("q") String searchQuery);
@GET
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
List<UserRepresentation> searchByAttributes(@QueryParam("q") String searchQuery,
@QueryParam("exact") Boolean exact);
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)

View file

@ -294,7 +294,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
return null; return null;
} }
StorageId clientStorageId = null; StorageId clientStorageId;
if ( entity.getClientId() == null) { if ( entity.getClientId() == null) {
clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId()); clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId());
} else { } else {
@ -625,7 +625,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
predicates.add(builder.or(getSearchOptionPredicateArray(stringToSearch, builder, root))); 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(); return em.createQuery(queryBuilder).getSingleResult().intValue();
} }
@ -654,7 +654,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
predicates.add(groupMembership.get("groupId").in(groupIds)); 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(); return em.createQuery(queryBuilder).getSingleResult().intValue();
} }
@ -670,7 +670,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
List<Predicate> restrictions = predicates(params, from, Map.of()); List<Predicate> restrictions = predicates(params, from, Map.of());
restrictions.add(qb.equal(from.get("realmId"), realm.getId())); 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<Long> query = em.createQuery(userQuery); TypedQuery<Long> query = em.createQuery(userQuery);
Long result = query.getSingleResult(); Long result = query.getSingleResult();
@ -695,7 +695,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
groupsWithPermissionsSubquery(countQuery, groupIds, root, restrictions); groupsWithPermissionsSubquery(countQuery, groupIds, root, restrictions);
countQuery.where(restrictions.toArray(new Predicate[0])); countQuery.where(restrictions.toArray(Predicate[]::new));
TypedQuery<Long> query = em.createQuery(countQuery); TypedQuery<Long> query = em.createQuery(countQuery);
Long result = query.getSingleResult(); 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)); 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) { 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("name"), key),
builder.equal(attributesJoin.get("longValueHashLowerCase"), JpaHashUtils.hashForAttributeValueLowerCase(value)))); builder.equal(attributesJoin.get("longValueHashLowerCase"), JpaHashUtils.hashForAttributeValueLowerCase(value))));
} else { } else {
attributePredicates.add(builder.and( if (Boolean.parseBoolean(attributes.get(UserModel.EXACT))) {
attributePredicates.add(builder.and(
builder.equal(attributesJoin.get("name"), key), builder.equal(attributesJoin.get("name"), key),
builder.equal(builder.lower(attributesJoin.get("value")), value.toLowerCase()))); 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; break;
case UserModel.INCLUDE_SERVICE_ACCOUNT: { case UserModel.INCLUDE_SERVICE_ACCOUNT: {
@ -1043,7 +1049,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
} }
if (!attributePredicates.isEmpty()) { if (!attributePredicates.isEmpty()) {
predicates.add(builder.and(attributePredicates.toArray(new Predicate[0]))); predicates.add(builder.and(attributePredicates.toArray(Predicate[]::new)));
} }
return predicates; return predicates;
@ -1074,11 +1080,11 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
Expression<String> groupId = from.get("groupId"); Expression<String> groupId = from.get("groupId");
subs.add(cb.like(from1.get("name"), cb.concat("group.resource.", 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)); subPredicates.add(cb.exists(subquery1));
subquery.where(subPredicates.toArray(new Predicate[0])); subquery.where(subPredicates.toArray(Predicate[]::new));
restrictions.add(cb.exists(subquery)); restrictions.add(cb.exists(subquery));
} }

View file

@ -918,13 +918,16 @@ public class UserTest extends AbstractAdminTest {
public void searchByMultipleAttributes() { public void searchByMultipleAttributes() {
createUsers(); createUsers();
Map<String, String> attributes = new HashMap<>(); List<UserRepresentation> users = realm.users().searchByAttributes(mapToSearchQuery(Map.of("username", "user", "test", "test1", "attr", "common", "test1", "test1")));
attributes.put("test", "test1"); assertThat(users, hasSize(1));
attributes.put("attr", "common");
attributes.put("test1", "test1");
List<UserRepresentation> users = realm.users().searchByAttributes(mapToSearchQuery(attributes)); //custom user attribute should use wildcard search by default
assertEquals(1, users.size()); 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 @Test