Add briefRepresentation to get organizations from user

Allow asking for the full representation in `GET /admin/realms/{realm}/organizations/members/{member-id}/organizations`

Closes #40438

Signed-off-by: Alexis Rico <sferadev@gmail.com>
This commit is contained in:
Alexis Rico
2025-06-16 20:05:35 +02:00
committed by GitHub
parent 160ec6ba0d
commit d4eec2ad32
7 changed files with 82 additions and 12 deletions

View File

@@ -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<OrganizationRepresentation> getOrganizations();
List<OrganizationRepresentation> getOrganizations(
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
}

View File

@@ -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<OrganizationRepresentation> getOrganizations(@PathParam("id") String id);
List<OrganizationRepresentation> getOrganizations(
@PathParam("id") String id,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
}

View File

@@ -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<OrganizationRepresentation> getOrganizations(@PathParam("id") String id);
List<OrganizationRepresentation> getOrganizations(
@PathParam("id") String id,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
}

View File

@@ -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<OrganizationRepresentation> getOrganizations(@PathParam("member-id") String memberId) {
public Stream<OrganizationRepresentation> 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")

View File

@@ -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<OrganizationRepresentation> getOrganizations(@PathParam("member-id") String memberId) {
return new OrganizationMemberResource(session, null, adminEvent).getOrganizations(memberId);
public Stream<OrganizationRepresentation> 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);
}
}

View File

@@ -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<OrganizationRepresentation> actual = organization.members().member(member.getId()).getOrganizations();
List<OrganizationRepresentation> 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<String> memberOfOrgs = orga.members().member(member.getId()).getOrganizations().stream().map(OrganizationRepresentation::getId).toList();
List<String> 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<OrganizationRepresentation> 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<OrganizationRepresentation> 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<OrganizationRepresentation> 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<OrganizationRepresentation> 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());

View File

@@ -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<OrganizationRepresentation> orgMemberships = organization.members().member(ldapUser.getId()).getOrganizations();
List<OrganizationRepresentation> orgMemberships = organization.members().member(ldapUser.getId()).getOrganizations(true);
assertThat(orgMemberships, notNullValue());
assertThat(orgMemberships, hasSize(1));
assertThat(orgMemberships.get(0).getId(), equalTo(orgRepresentation.getId()));