From 51fa054ba77b3314f3fe64e94556e9ce66d20fcc Mon Sep 17 00:00:00 2001 From: Martin Kanis Date: Tue, 9 Apr 2024 14:45:11 +0200 Subject: [PATCH] Manage organization attributes Closes #28253 Signed-off-by: Martin Kanis --- .../idm/OrganizationRepresentation.java | 20 +++++++++ .../organization/jpa/OrganizationAdapter.java | 43 +++++++++++++++++++ .../keycloak/models/OrganizationModel.java | 16 +++++++ .../admin/resource/OrganizationResource.java | 10 +++++ .../organization/admin/OrganizationTest.java | 42 ++++++++++++++++++ 5 files changed, 131 insertions(+) diff --git a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java index 352976faea..4fb2b9f2b5 100644 --- a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -17,10 +17,16 @@ package org.keycloak.representations.idm; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + public class OrganizationRepresentation { private String id; private String name; + private Map> attributes = new HashMap<>(); public String getId() { return id; @@ -38,6 +44,20 @@ public class OrganizationRepresentation { return name; } + public Map> getAttributes() { + return attributes; + } + + public void setAttributes(Map> attributes) { + this.attributes = attributes; + } + + public OrganizationRepresentation singleAttribute(String name, String value) { + if (this.attributes == null) attributes = new HashMap<>(); + attributes.put(name, Arrays.asList(value)); + return this; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java index 731f5a4f3e..4fb64b121c 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java @@ -17,15 +17,21 @@ package org.keycloak.organization.jpa; +import org.keycloak.models.GroupModel; import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.jpa.JpaModel; import org.keycloak.models.jpa.entities.OrganizationEntity; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + public final class OrganizationAdapter implements OrganizationModel, JpaModel { private final RealmModel realm; private final OrganizationEntity entity; + private GroupModel group; public OrganizationAdapter(RealmModel realm, OrganizationEntity entity) { this.realm = realm; @@ -55,11 +61,48 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel values) { + getGroup().setAttribute(name, values); + } + + @Override + public void removeAttribute(String name) { + getGroup().removeAttribute(name); + } + + @Override + public String getFirstAttribute(String name) { + return getGroup().getFirstAttribute(name); + } + + @Override + public Stream getAttributeStream(String name) { + return getGroup().getAttributeStream(name); + } + + @Override + public Map> getAttributes() { + return getGroup().getAttributes(); + } + @Override public OrganizationEntity getEntity() { return entity; } + private GroupModel getGroup() { + if (group == null) { + group = realm.getGroupById(getGroupId()); + } + return group; + } + @Override public String toString() { return new StringBuilder() diff --git a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java index a58b2a6aab..169e47d547 100644 --- a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java @@ -17,6 +17,10 @@ package org.keycloak.models; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + public interface OrganizationModel { String USER_ORGANIZATION_ATTRIBUTE = "kc.org"; @@ -26,4 +30,16 @@ public interface OrganizationModel { void setName(String name); String getName(); + + void setSingleAttribute(String name, String value); + + void setAttribute(String name, List values); + + void removeAttribute(String name); + + String getFirstAttribute(String name); + + Stream getAttributeStream(String name); + + Map> getAttributes(); } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java index 25ed0af745..2eda49d9ca 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java @@ -17,6 +17,7 @@ package org.keycloak.organization.admin.resource; +import java.util.Set; import java.util.stream.Stream; import jakarta.ws.rs.BadRequestException; @@ -141,6 +142,7 @@ public class OrganizationResource { rep.setId(model.getId()); rep.setName(model.getName()); + rep.setAttributes(model.getAttributes()); return rep; } @@ -152,6 +154,14 @@ public class OrganizationResource { model.setName(rep.getName()); + if (rep.getAttributes() != null) { + Set attrsToRemove = model.getAttributes().keySet(); + attrsToRemove.removeAll(rep.getAttributes().keySet()); + attrsToRemove.forEach(model::removeAttribute); + + rep.getAttributes().entrySet().forEach(entry -> model.setAttribute(entry.getKey(), entry.getValue())); + } + return model; } } 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 ce350a5edc..bdc75970eb 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 @@ -20,9 +20,11 @@ package org.keycloak.testsuite.organization.admin; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import jakarta.ws.rs.NotFoundException; @@ -93,4 +95,44 @@ public class OrganizationTest extends AbstractOrganizationTest { fail("should be deleted"); } catch (NotFoundException ignore) {} } + + @Test + public void testAttributes() { + OrganizationRepresentation org = createOrganization(); + org = org.singleAttribute("key", "value"); + + OrganizationResource organization = testRealm().organizations().get(org.getId()); + + try (Response response = organization.update(org)) { + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + + OrganizationRepresentation updated = organization.toRepresentation(); + assertEquals(org.getAttributes().get("key"), updated.getAttributes().get("key")); + + HashMap> attributes = new HashMap<>(); + attributes.put("attr1", List.of("val11", "val12")); + attributes.put("attr2", List.of("val21", "val22")); + org.setAttributes(attributes); + + try (Response response = organization.update(org)) { + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + + updated = organization.toRepresentation(); + assertNull(updated.getAttributes().get("key")); + assertEquals(2, updated.getAttributes().size()); + assertEquals(org.getAttributes().get("attr1"), updated.getAttributes().get("attr1")); + assertEquals(org.getAttributes().get("attr2"), updated.getAttributes().get("attr2")); + + attributes.clear(); + org.setAttributes(attributes); + + try (Response response = organization.update(org)) { + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + + updated = organization.toRepresentation(); + assertEquals(0, updated.getAttributes().size()); + } }