From 273565746c6d83eb9e02a58a1b12ba3d10eabcb0 Mon Sep 17 00:00:00 2001 From: Michael-AT-Corporation Date: Mon, 2 Jun 2025 19:12:28 +0200 Subject: [PATCH] Added new searchByAttributes function to UsersResource with the exact parameter Closes #39609 Signed-off-by: Michael-AT-Corporation --- .../topics/changes/changes-26_4_0.adoc | 36 +++++++++++++++++++ .../upgrading/topics/changes/changes.adoc | 4 +-- .../admin/client/resource/UsersResource.java | 10 ++++++ .../keycloak/testsuite/admin/UserTest.java | 28 +++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc diff --git a/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc new file mode 100644 index 000000000000..8b96fca9d6ae --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc @@ -0,0 +1,36 @@ +== Breaking changes + +Breaking changes are identified as requiring changes from existing users to their configurations. +In minor or patch releases we will only do breaking changes to fix bugs. + +=== + +== Notable changes + +Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}. + +=== Usage of the `exact` request parameter when searching users by attributes + +If you are querying users by attributes through the User API where you want to fetch users that match a specific attribute key (regardless the value), +you should consider setting the `exact` request parameter to `false` when invoking the `/admin/realms/{realm}/users` using +the `GET` method. + +For instance, searching for all users with the attribute `myattribute` set should be done as follows: + +``` +GET /admin/realms/{realm}/users?exact=false&q=myattribute: +``` + +The {project_name} Admin Client is also updated with a new method to search users by attribute using the `exact` request parameter. + +== Deprecated features + +The following sections provide details on deprecated features. + +=== + +== Removed features + +The following features have been removed from this release. + +=== diff --git a/docs/documentation/upgrading/topics/changes/changes.adoc b/docs/documentation/upgrading/topics/changes/changes.adoc index a7e53c0295a8..ea7f6ba5d3c3 100644 --- a/docs/documentation/upgrading/topics/changes/changes.adoc +++ b/docs/documentation/upgrading/topics/changes/changes.adoc @@ -1,9 +1,9 @@ [[migration-changes]] == Migration Changes -=== Migrating to 26.3.1 +=== Migrating to 26.4.0 -include::changes-26_3_1.adoc[leveloffset=2] +include::changes-26_4_0.adoc[leveloffset=2] === Migrating to 26.3.0 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 cf7d6c8ef4e8..cbe8f46f4c0a 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 @@ -129,6 +129,16 @@ List searchByAttributes(@QueryParam("first") Integer firstRe @QueryParam("briefRepresentation") Boolean briefRepresentation, @QueryParam("q") String searchQuery); + @GET + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + List searchByAttributes(@QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, + @QueryParam("enabled") Boolean enabled, + @QueryParam("exact") Boolean exact, + @QueryParam("briefRepresentation") Boolean briefRepresentation, + @QueryParam("q") String searchQuery); + @GET @Produces(MediaType.APPLICATION_JSON) List search(@QueryParam("username") String username, @QueryParam("exact") Boolean exact); 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 aad1e89800d6..d490a5503bc9 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 @@ -127,6 +127,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; @@ -961,6 +962,33 @@ public void searchByAttributesWithPagination() { } } + @Test + public void searchByAttributesForAnyValue() { + createUser(UserBuilder.create().username("user-0").addAttribute("attr", "common").build()); + createUser(UserBuilder.create().username("user-1").addAttribute("test", "common").build()); + createUser(UserBuilder.create().username("user-2").addAttribute("test", "common").addAttribute("attr", "common").build()); + + Map attributes = new HashMap<>(); + attributes.put("attr", ""); + // exact needs to be set to false to match for any users with the attribute attr + List users = realm.users().searchByAttributes(-1, -1, null, false, false, mapToSearchQuery(attributes)); + assertEquals(2, users.size()); + assertTrue(users.stream().allMatch(r -> Set.of("user-0", "user-2").contains(r.getUsername()))); + + attributes = new HashMap<>(); + attributes.put("test", ""); + users = realm.users().searchByAttributes(-1, -1, null, false, false, mapToSearchQuery(attributes)); + assertEquals(2, users.size()); + assertTrue(users.stream().allMatch(r -> Set.of("user-1", "user-2").contains(r.getUsername()))); + + attributes = new HashMap<>(); + attributes.put("test", ""); + attributes.put("attr", ""); + users = realm.users().searchByAttributes(-1, -1, null, false, false, mapToSearchQuery(attributes)); + assertEquals(1, users.size()); + assertTrue(users.stream().allMatch(r -> "user-2".equals(r.getUsername()))); + } + @Test public void storeAndReadUserWithLongAttributeValue() { String longValue = RandomStringUtils.random(Integer.parseInt(DefaultAttributes.DEFAULT_MAX_LENGTH_ATTRIBUTES), true, true);