diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java index f234b131ba1..cca5ec65f85 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMemberResource.java @@ -20,9 +20,11 @@ package org.keycloak.admin.client.resource; import java.util.List; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.keycloak.representations.idm.MemberRepresentation; @@ -46,5 +48,6 @@ public interface OrganizationMemberResource { @Path("organizations") @GET @Produces(MediaType.APPLICATION_JSON) - List getOrganizations(); + List getOrganizations( + @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java index b076658291f..89e289cce08 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationMembersResource.java @@ -21,6 +21,7 @@ import java.util.List; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -139,5 +140,7 @@ public interface OrganizationMembersResource { @Path("{id}/organizations") @GET @Produces(MediaType.APPLICATION_JSON) - List getOrganizations(@PathParam("id") String id); + List getOrganizations( + @PathParam("id") String id, + @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java index f70b1d7eb1e..6e6c189710e 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/OrganizationsMembersResource.java @@ -19,10 +19,12 @@ package org.keycloak.admin.client.resource; import java.util.List; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import org.keycloak.representations.idm.OrganizationRepresentation; @@ -31,5 +33,7 @@ public interface OrganizationsMembersResource { @Path("{id}/organizations") @GET @Produces(MediaType.APPLICATION_JSON) - List getOrganizations(@PathParam("id") String id); + List getOrganizations( + @PathParam("id") String id, + @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation); } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java index b1fcfcccd34..3f825096620 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java @@ -241,14 +241,18 @@ public class OrganizationMemberResource { @APIResponse(responseCode = "200", description = "", content = @Content(schema = @Schema(implementation = OrganizationRepresentation.class, type = SchemaType.ARRAY))), @APIResponse(responseCode = "400", description = "Bad Request") }) - public Stream getOrganizations(@PathParam("member-id") String memberId) { + public Stream getOrganizations( + @PathParam("member-id") String memberId, + @Parameter(description = "if false, return the full representation. Otherwise, only the basic fields are returned.") + @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { if (StringUtil.isBlank(memberId)) { throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); } UserModel member = getUser(memberId); - return provider.getByMember(member).map(ModelToRepresentation::toRepresentation); + return provider.getByMember(member) + .map(model -> ModelToRepresentation.toRepresentation(model, briefRepresentation)); } @Path("count") 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 777986507bd..bf9451d1f6c 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 @@ -222,7 +222,10 @@ public class OrganizationsResource { @APIResponse(responseCode = "200", description = "", content = @Content(schema = @Schema(implementation = OrganizationRepresentation.class, type = SchemaType.ARRAY))), @APIResponse(responseCode = "400", description = "Bad Request") }) - public Stream getOrganizations(@PathParam("member-id") String memberId) { - return new OrganizationMemberResource(session, null, adminEvent).getOrganizations(memberId); + public Stream getOrganizations( + @PathParam("member-id") String memberId, + @Parameter(description = "if false, return the full representation. Otherwise, only the basic fields are returned.") + @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + return new OrganizationMemberResource(session, null, adminEvent).getOrganizations(memberId, briefRepresentation); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberTest.java index 052058e3260..f450ab6845b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberTest.java @@ -123,13 +123,13 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { OrganizationRepresentation orgB = createOrganization("orgb"); testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close(); OrganizationRepresentation expected = organization.toRepresentation(); - List actual = organization.members().member(member.getId()).getOrganizations(); + List actual = organization.members().member(member.getId()).getOrganizations(true); assertNotNull(actual); assertEquals(2, actual.size()); assertTrue(actual.stream().map(OrganizationRepresentation::getId).anyMatch(expected.getId()::equals)); assertTrue(actual.stream().map(OrganizationRepresentation::getId).anyMatch(orgB.getId()::equals)); - actual = testRealm().organizations().members().getOrganizations(member.getId()); + actual = testRealm().organizations().members().getOrganizations(member.getId(), true); assertNotNull(actual); assertEquals(2, actual.size()); assertTrue(actual.stream().map(OrganizationRepresentation::getId).anyMatch(expected.getId()::equals)); @@ -332,7 +332,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { for (MemberRepresentation member : expected) { try { // user no longer bound to the organization - organization.members().member(member.getId()).getOrganizations(); + organization.members().member(member.getId()).getOrganizations(true); fail("should not be associated with the organization anymore"); } catch (NotFoundException ignore) { } @@ -507,7 +507,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { Assert.assertTrue(orgb.members().list(-1, -1).stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals)); String orgbId = orgb.toRepresentation().getId(); String orgaId = orga.toRepresentation().getId(); - List memberOfOrgs = orga.members().member(member.getId()).getOrganizations().stream().map(OrganizationRepresentation::getId).toList(); + List memberOfOrgs = orga.members().member(member.getId()).getOrganizations(true).stream().map(OrganizationRepresentation::getId).toList(); assertTrue(memberOfOrgs.contains(orgaId)); assertTrue(memberOfOrgs.contains(orgbId)); } @@ -583,6 +583,59 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { assertThat(updatedUser.getEmail(), is(nullValue())); } + @Test + public void testGetMemberOrganizationsBriefVsFullRepresentation() { + // Create an organization with attributes + OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); + OrganizationRepresentation orgRep = organization.toRepresentation(); + orgRep.singleAttribute("testAttribute", "testValue"); + organization.update(orgRep).close(); + + UserRepresentation member = addMember(organization); + + // Test brief representation (default, briefRepresentation=true) + List briefOrgs = organization.members().member(member.getId()).getOrganizations(true); + assertNotNull(briefOrgs); + assertEquals(1, briefOrgs.size()); + OrganizationRepresentation briefRep = briefOrgs.get(0); + assertEquals(orgRep.getId(), briefRep.getId()); + assertEquals(orgRep.getName(), briefRep.getName()); + assertEquals(orgRep.getAlias(), briefRep.getAlias()); + assertEquals(orgRep.getDescription(), briefRep.getDescription()); + assertEquals(orgRep.getRedirectUrl(), briefRep.getRedirectUrl()); + assertEquals(orgRep.isEnabled(), briefRep.isEnabled()); + assertNull("Brief representation should not include attributes", briefRep.getAttributes()); + + // Test full representation (briefRepresentation=false) + List fullOrgs = organization.members().member(member.getId()).getOrganizations(false); + assertNotNull(fullOrgs); + assertEquals(1, fullOrgs.size()); + OrganizationRepresentation fullRep = fullOrgs.get(0); + assertEquals(orgRep.getId(), fullRep.getId()); + assertEquals(orgRep.getName(), fullRep.getName()); + assertEquals(orgRep.getAlias(), fullRep.getAlias()); + assertEquals(orgRep.getDescription(), fullRep.getDescription()); + assertEquals(orgRep.getRedirectUrl(), fullRep.getRedirectUrl()); + assertEquals(orgRep.isEnabled(), fullRep.isEnabled()); + assertNotNull("Full representation should include attributes", fullRep.getAttributes()); + assertTrue("Full representation should include the test attribute", + fullRep.getAttributes().containsKey("testAttribute")); + assertEquals("testValue", fullRep.getAttributes().get("testAttribute").get(0)); + + // Test the global members API endpoint as well + List briefOrgsGlobal = testRealm().organizations().members().getOrganizations(member.getId(), true); + assertNotNull(briefOrgsGlobal); + assertEquals(1, briefOrgsGlobal.size()); + assertNull("Brief representation should not include attributes", briefOrgsGlobal.get(0).getAttributes()); + + List fullOrgsGlobal = testRealm().organizations().members().getOrganizations(member.getId(), false); + assertNotNull(fullOrgsGlobal); + assertEquals(1, fullOrgsGlobal.size()); + assertNotNull("Full representation should include attributes", fullOrgsGlobal.get(0).getAttributes()); + assertTrue("Full representation should include the test attribute", + fullOrgsGlobal.get(0).getAttributes().containsKey("testAttribute")); + } + private void loginViaNonOrgIdP(String idpAlias) { oauth.clientId("broker-app"); loginPage.open(bc.consumerRealmName()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberWithLdapTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberWithLdapTest.java index 5f65fc48de9..cc0a45930b4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberWithLdapTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/member/OrganizationMemberWithLdapTest.java @@ -89,7 +89,7 @@ public class OrganizationMemberWithLdapTest extends AbstractOrganizationTest { try (Response response = organization.members().addMember(ldapUser.getId())) { assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); } - List orgMemberships = organization.members().member(ldapUser.getId()).getOrganizations(); + List orgMemberships = organization.members().member(ldapUser.getId()).getOrganizations(true); assertThat(orgMemberships, notNullValue()); assertThat(orgMemberships, hasSize(1)); assertThat(orgMemberships.get(0).getId(), equalTo(orgRepresentation.getId()));