From cc64375c8804debdd365ce314f284ee3008a59bd Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 22 Nov 2024 07:50:28 -0300 Subject: [PATCH] Allow returning attributes when querying organizations Closes #34590 Signed-off-by: Himanshi Gupta Signed-off-by: Pedro Igor Co-authored-by: Himanshi Gupta --- .../resource/OrganizationsResource.java | 22 +++++++++++++++ .../models/utils/ModelToRepresentation.java | 7 +++-- .../admin/resource/OrganizationsResource.java | 11 ++++---- .../organization/admin/OrganizationTest.java | 27 ++++++++++++++----- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java index ed7c3e093de..31ff6390a55 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java @@ -83,6 +83,28 @@ public interface OrganizationsResource { @QueryParam("max") Integer max ); + /** + * Returns all organizations that match the specified filters. + * + * @param search a {@code String} representing either an organization name or domain. + * @param exact if {@code true}, the organizations will be searched using exact match for the {@code search} param - i.e. + * either the organization name or one of its domains must match exactly the {@code search} param. If false, + * the method returns all organizations whose name or (domains) partially match the {@code search} param. + * @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}. + * @param max the maximum number of results to be returned. Ignored if negative or {@code null}. + * @param briefRepresentation if {@code true} the full representation is to be returned. Otherwise, only the basic fields are returned. + * @return a list containing the matched organizations. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + List search( + @QueryParam("search") String search, + @QueryParam("exact") Boolean exact, + @QueryParam("first") Integer first, + @QueryParam("max") Integer max, + @QueryParam("briefRepresentation") Boolean briefRepresentation + ); + /** * Returns all organizations that contain attributes matching the specified query. * diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 7b36e62e92e..dd410a0dc37 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -1296,7 +1296,7 @@ public class ModelToRepresentation { } public static OrganizationRepresentation toRepresentation(OrganizationModel model) { - OrganizationRepresentation rep = toBriefRepresentation(model); + OrganizationRepresentation rep = toBriefRepresentation(model,false); if (rep == null) { return null; } @@ -1304,7 +1304,7 @@ public class ModelToRepresentation { return rep; } - public static OrganizationRepresentation toBriefRepresentation(OrganizationModel model) { + public static OrganizationRepresentation toBriefRepresentation(OrganizationModel model, Boolean briefRepresentation) { if (model == null) { return null; } @@ -1312,6 +1312,9 @@ public class ModelToRepresentation { rep.setId(model.getId()); rep.setName(model.getName()); rep.setAlias(model.getAlias()); + if (briefRepresentation) { + rep.setAttributes(model.getAttributes()); + } rep.setEnabled(model.isEnabled()); rep.setRedirectUrl(model.getRedirectUrl()); rep.setDescription(model.getDescription()); diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java index 5f17c2e89ab..f19b85ffb01 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java @@ -127,23 +127,24 @@ public class OrganizationsResource { @Produces(MediaType.APPLICATION_JSON) @NoCache @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) - @Operation( summary = "Returns a paginated list of organizations filtered according to the specified parameters") + @Operation(summary = "Returns a paginated list of organizations filtered according to the specified parameters") public Stream search( @Parameter(description = "A String representing either an organization name or domain") @QueryParam("search") String search, @Parameter(description = "A query to search for custom attributes, in the format 'key1:value2 key2:value2'") @QueryParam("q") String searchQuery, @Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact, @Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first, - @Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max - ) { + @Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max, + @Parameter(description = "if true, return the full representation. Otherwise, only the basic fields are returned.") @QueryParam("briefRepresentation") @DefaultValue("false") boolean briefRepresentation + ) { auth.realm().requireManageRealm(); Organizations.checkEnabled(provider); // check if are searching orgs by attribute. if (StringUtil.isNotBlank(searchQuery)) { Map attributes = SearchQueryUtils.getFields(searchQuery); - return provider.getAllStream(attributes, first, max).map(ModelToRepresentation::toBriefRepresentation); + return provider.getAllStream(attributes, first, max).map(model -> ModelToRepresentation.toBriefRepresentation(model, briefRepresentation)); } else { - return provider.getAllStream(search, exact, first, max).map(ModelToRepresentation::toBriefRepresentation); + return provider.getAllStream(search, exact, first, max).map(model -> ModelToRepresentation.toBriefRepresentation(model, briefRepresentation)); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java index 7e039fbb90f..bd4777197f5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java @@ -21,6 +21,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -43,6 +44,7 @@ import java.util.stream.Collectors; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; + import java.io.IOException; import java.util.stream.IntStream; @@ -147,6 +149,15 @@ public class OrganizationTest extends AbstractOrganizationTest { assertThat(orgRep.getDomain("gtbank.net"), not(nullValue())); assertThat(orgRep.getAttributes(), nullValue()); + orgRep.singleAttribute("foo", "bar"); + orgRep.singleAttribute("bar", "foo"); + testRealm().organizations().get(orgRep.getId()).update(orgRep).close(); + existing = testRealm().organizations().search("gtbank.net", true, 0, 10, true); + assertThat(existing, hasSize(1)); + orgRep = existing.get(0); + assertThat(orgRep.getAttributes(), notNullValue()); + assertThat(2, is(orgRep.getAttributes().size())); + existing = testRealm().organizations().search("nonexistent.org", true, 0, 10); assertThat(existing, is(empty())); @@ -268,7 +279,8 @@ public class OrganizationTest extends AbstractOrganizationTest { try { organization.toRepresentation(); fail("should be deleted"); - } catch (NotFoundException ignore) {} + } catch (NotFoundException ignore) { + } } @Test @@ -414,15 +426,18 @@ public class OrganizationTest extends AbstractOrganizationTest { try { testRealm().organizations().getAll(); fail("Expected NotFoundException"); - } catch (NotFoundException expected) {} + } catch (NotFoundException expected) { + } try { testRealm().organizations().search("*"); fail("Expected NotFoundException"); - } catch (NotFoundException expected) {} + } catch (NotFoundException expected) { + } try { testRealm().organizations().get(existing.getId()).toRepresentation(); fail("Expected NotFoundException"); - } catch (NotFoundException expected) {} + } catch (NotFoundException expected) { + } } } @@ -461,8 +476,8 @@ public class OrganizationTest extends AbstractOrganizationTest { @Test public void testCount() { List orgIds = IntStream.range(0, 10) - .mapToObj(i -> createOrganization("kc.org." + i).getId()) - .collect(Collectors.toList()); + .mapToObj(i -> createOrganization("kc.org." + i).getId()) + .collect(Collectors.toList()); getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) session -> { OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);