From 9a466f90ab93db805cfdeab2612a3ee29f9ceb5c Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Wed, 10 Apr 2024 08:51:36 -0300 Subject: [PATCH] Add ability to set one or more internet domain to an organization. Closed #28274 Signed-off-by: Stefan Guilhen --- .../idm/OrganizationDomainRepresentation.java | 63 ++++++++++++ .../idm/OrganizationRepresentation.java | 15 +++ .../resource/OrganizationsResource.java | 5 +- .../entities/OrganizationDomainEntity.java | 95 +++++++++++++++++++ .../jpa/entities/OrganizationEntity.java | 29 ++++++ .../jpa/JpaOrganizationProvider.java | 14 +++ .../organization/jpa/OrganizationAdapter.java | 45 ++++++++- .../META-INF/jpa-changelog-25.0.0.xml | 16 +++- .../main/resources/default-persistence.xml | 1 + .../organization/OrganizationProvider.java | 20 ++-- .../models/OrganizationDomainModel.java | 70 ++++++++++++++ .../keycloak/models/OrganizationModel.java | 5 + .../admin/resource/OrganizationResource.java | 49 +++++++++- .../admin/AbstractOrganizationTest.java | 12 +++ .../organization/admin/OrganizationTest.java | 75 ++++++++++++++- 15 files changed, 498 insertions(+), 16 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java create mode 100644 model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationDomainEntity.java create mode 100644 server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java diff --git a/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java new file mode 100644 index 0000000000..c0d2b053f7 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +/** + * Representation implementation of an organization internet domain. + * + * @author Stefan Guilhen + */ +public class OrganizationDomainRepresentation { + + private String name; + private Boolean verified; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Boolean isVerified() { + return this.verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof OrganizationDomainRepresentation)) return false; + + OrganizationDomainRepresentation that = (OrganizationDomainRepresentation) o; + return name != null && name.equals(that.getName()); + } + + @Override + public int hashCode() { + if (name == null) { + return super.hashCode(); + } + return name.hashCode(); + } +} 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 4fb2b9f2b5..0f38fb4837 100644 --- a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -21,12 +21,15 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.HashSet; +import java.util.Set; public class OrganizationRepresentation { private String id; private String name; private Map> attributes = new HashMap<>(); + private Set domains = new HashSet<>(); public String getId() { return id; @@ -58,6 +61,18 @@ public class OrganizationRepresentation { return this; } + public Set getDomains() { + return this.domains; + } + + public void addDomain(OrganizationDomainRepresentation domain) { + this.domains.add(domain); + } + + public void removeDomain(OrganizationDomainRepresentation domain) { + this.domains.remove(domain); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java index f6972ce9f9..3dc1ca97b1 100644 --- a/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java +++ b/integration/admin-client-jee/src/main/java/org/keycloak/admin/client/resource/OrganizationsResource.java @@ -25,6 +25,7 @@ import jakarta.ws.rs.POST; 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 jakarta.ws.rs.core.Response; import org.keycloak.representations.idm.OrganizationRepresentation; @@ -40,5 +41,7 @@ public interface OrganizationsResource { @GET @Produces(MediaType.APPLICATION_JSON) - List getAll(); + List getAll( + @QueryParam("domain-name") String domainName + ); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationDomainEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationDomainEntity.java new file mode 100644 index 0000000000..8af63978e3 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationDomainEntity.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.jpa.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.Table; + +/** + * JPA entity representing an internet domain that can be associated with an organization. + * + * @author Stefan Guilhen + */ +@Entity +@Table(name="ORG_DOMAIN") +@NamedQueries({ + @NamedQuery(name="getByName", query="select o from OrganizationDomainEntity o where o.name = :name") +}) +public class OrganizationDomainEntity { + + @Id + @Column(name="NAME") + protected String name; + + @Column(name="VERIFIED") + protected Boolean verified; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ORG_ID") + private OrganizationEntity organization; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Boolean isVerified() { + return this.verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } + + public OrganizationEntity getOrganization() { + return this.organization; + } + + public void setOrganization(OrganizationEntity organization) { + this.organization = organization; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof OrganizationDomainEntity)) return false; + + OrganizationDomainEntity that = (OrganizationDomainEntity) o; + return name != null && name.equals(that.getName()); + } + + @Override + public int hashCode() { + if (name == null) { + return super.hashCode(); + } + return name.hashCode(); + } + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java index 566f5a5acb..5eed20202d 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java @@ -17,14 +17,23 @@ package org.keycloak.models.jpa.entities; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + import jakarta.persistence.Access; import jakarta.persistence.AccessType; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; @Table(name="ORG") @Entity @@ -50,6 +59,11 @@ public class OrganizationEntity { @Column(name = "IPD_ALIAS") private String idpAlias; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy="organization") + @Fetch(FetchMode.SELECT) + @BatchSize(size = 20) + protected Set domains = new HashSet<>(); + public String getId() { return id; } @@ -90,6 +104,21 @@ public class OrganizationEntity { this.idpAlias = idpAlias; } + public Collection getDomains() { + if (this.domains == null) { + this.domains = new HashSet<>(); + } + return this.domains; + } + + public void addDomain(OrganizationDomainEntity domainEntity) { + this.domains.add(domainEntity); + } + + public void removeDomain(OrganizationDomainEntity domainEntity) { + this.domains.remove(domainEntity); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java index 902a7e3f3d..77ca68f45a 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java @@ -23,6 +23,7 @@ import static org.keycloak.utils.StreamsUtil.closing; import java.util.stream.Stream; import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; import jakarta.persistence.TypedQuery; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.models.GroupModel; @@ -35,6 +36,7 @@ import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; +import org.keycloak.models.jpa.entities.OrganizationDomainEntity; import org.keycloak.models.jpa.entities.OrganizationEntity; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.organization.OrganizationProvider; @@ -122,6 +124,18 @@ public class JpaOrganizationProvider implements OrganizationProvider { return entity == null ? null : new OrganizationAdapter(realm, entity); } + @Override + public OrganizationModel getByDomainName(String domain) { + TypedQuery query = em.createNamedQuery("getByName", OrganizationDomainEntity.class); + query.setParameter("name", domain.toLowerCase()); + try { + OrganizationDomainEntity entity = query.getSingleResult(); + return new OrganizationAdapter(realm, entity.getOrganization()); + } catch (NoResultException nre) { + return null; + } + } + @Override public Stream getAllStream() { TypedQuery query = em.createNamedQuery("getByRealm", OrganizationEntity.class); 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 4fb64b121c..31deb133b7 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 @@ -18,14 +18,20 @@ package org.keycloak.organization.jpa; import org.keycloak.models.GroupModel; +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.jpa.JpaModel; +import org.keycloak.models.jpa.entities.OrganizationDomainEntity; 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 { @@ -91,6 +97,37 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel getDomains() { + return entity.getDomains().stream().map(this::toModel); + } + + @Override + public void setDomains(Collection domains) { + Map modelMap = domains.stream() + .collect(Collectors.toMap(model -> model.getName(), Function.identity())); + for (OrganizationDomainEntity domainEntity : this.entity.getDomains()) { + // update the existing domain (for now, only the verified flag can be changed). + if (modelMap.containsKey(domainEntity.getName())) { + domainEntity.setVerified(modelMap.get(domainEntity.getName()).getVerified()); + modelMap.remove(domainEntity.getName()); + } + // remove domain that is not found in the new set. + else { + this.entity.removeDomain(domainEntity); + } + } + + // create the remaining domains. + for (OrganizationDomainModel model : modelMap.values()) { + OrganizationDomainEntity domainEntity = new OrganizationDomainEntity(); + domainEntity.setName(model.getName().toLowerCase()); + domainEntity.setVerified(model.getVerified() == null ? Boolean.FALSE : model.getVerified()); + domainEntity.setOrganization(this.entity); + this.entity.addDomain(domainEntity); + } + } + @Override public OrganizationEntity getEntity() { return entity; @@ -118,4 +155,8 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel - + @@ -90,10 +90,20 @@ - - + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/default-persistence.xml b/model/jpa/src/main/resources/default-persistence.xml index bcc0da0ac1..50777c8940 100644 --- a/model/jpa/src/main/resources/default-persistence.xml +++ b/model/jpa/src/main/resources/default-persistence.xml @@ -86,6 +86,7 @@ org.keycloak.models.jpa.entities.OrganizationEntity + org.keycloak.models.jpa.entities.OrganizationDomainEntity true diff --git a/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java b/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java index 38f597cc3f..a7b82db58e 100644 --- a/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/organization/OrganizationProvider.java @@ -45,6 +45,20 @@ public interface OrganizationProvider extends Provider { */ OrganizationModel getById(String id); + /** + * Returns a {@link OrganizationModel} by its internet domain. + * + * @param domainName the organization's internet domain (e.g. redhat.com) + * @return the organization that is linked to the given internet domain + */ + OrganizationModel getByDomainName(String domainName); + + /** + * Returns the organizations of the given realm as a stream. + * @return Stream of the organizations. Never returns {@code null}. + */ + Stream getAllStream(); + /** * Removes the given organization from the realm together with the data associated with it, e.g. its members etc. * @@ -69,12 +83,6 @@ public interface OrganizationProvider extends Provider { */ boolean addMember(OrganizationModel organization, UserModel user); - /** - * Returns the organizations of the realm as a stream. - * @return Stream of the organizations. Never returns {@code null}. - */ - Stream getAllStream(); - /** * Returns the members of a given {@link OrganizationModel}. * diff --git a/server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java b/server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java new file mode 100644 index 0000000000..38dfdf9833 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models; + +import java.io.Serializable; + +/** + * Model implementation of an organization internet domain. + * + * @author Stefan Guilhen + */ +public class OrganizationDomainModel implements Serializable { + + private String name; + private Boolean verified; + + public OrganizationDomainModel(String name, Boolean verified) { + this.name = name; + this.verified = verified; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Boolean getVerified() { + return this.verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof OrganizationDomainModel)) return false; + + OrganizationDomainModel that = (OrganizationDomainModel) o; + return name != null && name.equals(that.getName()); + } + + @Override + public int hashCode() { + if (name == null) { + return super.hashCode(); + } + return name.hashCode(); + } +} 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 169e47d547..ff7bf63dd6 100644 --- a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java @@ -19,6 +19,7 @@ package org.keycloak.models; import java.util.List; import java.util.Map; +import java.util.Collection; import java.util.stream.Stream; public interface OrganizationModel { @@ -42,4 +43,8 @@ public interface OrganizationModel { Stream getAttributeStream(String name); Map> getAttributes(); + + Stream getDomains(); + + void setDomains(Collection domains); } 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 98963cb244..654b094254 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 @@ -18,6 +18,8 @@ package org.keycloak.organization.admin.resource; import java.util.Set; +import java.util.Objects; +import java.util.stream.Collectors; import java.util.stream.Stream; import jakarta.ws.rs.BadRequestException; @@ -30,13 +32,18 @@ import jakarta.ws.rs.PUT; 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 jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationModel; import org.keycloak.organization.OrganizationProvider; +import org.keycloak.representations.idm.OrganizationDomainRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; +import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.utils.StringUtil; @@ -69,14 +76,23 @@ public class OrganizationResource { } OrganizationModel model = provider.create(organization.getName()); + toModel(organization, model); return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build(); } @GET @Produces(MediaType.APPLICATION_JSON) - public Stream get() { - return provider.getAllStream().map(this::toRepresentation); + public Stream search( + @Parameter(description = "A String representing an organization internet domain") @QueryParam("domain-name") String domainName + ) { + if (domainName == null || domainName.trim().isEmpty()) { + return provider.getAllStream().map(this::toRepresentation); + } else { + // search for the organization associated with the given domain + OrganizationModel org = provider.getByDomainName(domainName.trim()); + return org == null ? Stream.empty() : Stream.of(toRepresentation(org)); + } } @Path("{id}") @@ -107,7 +123,6 @@ public class OrganizationResource { @Consumes(MediaType.APPLICATION_JSON) public Response update(@PathParam("id") String id, OrganizationRepresentation organization) { OrganizationModel model = getOrganization(id); - toModel(organization, model); return Response.noContent().build(); @@ -148,10 +163,19 @@ public class OrganizationResource { rep.setId(model.getId()); rep.setName(model.getName()); rep.setAttributes(model.getAttributes()); + model.getDomains().filter(Objects::nonNull).map(this::toRepresentation) + .forEach(rep::addDomain); return rep; } + private OrganizationDomainRepresentation toRepresentation(OrganizationDomainModel model) { + OrganizationDomainRepresentation representation = new OrganizationDomainRepresentation(); + representation.setName(model.getName()); + representation.setVerified(model.getVerified()); + return representation; + } + private OrganizationModel toModel(OrganizationRepresentation rep, OrganizationModel model) { if (rep == null) { return null; @@ -167,6 +191,25 @@ public class OrganizationResource { rep.getAttributes().entrySet().forEach(entry -> model.setAttribute(entry.getKey(), entry.getValue())); } + if (rep.getDomains() != null) { + model.setDomains(rep.getDomains().stream().filter(this::validateDomainRepresentation) + .peek(domainRep -> { + OrganizationModel orgModel = provider.getByDomainName(domainRep.getName()); + if (orgModel != null && !Objects.equals(model.getId(), orgModel.getId())) { + throw ErrorResponse.error("Domain " + domainRep.getName() + " is already linked to another organization", Response.Status.BAD_REQUEST); + } + }) + .map(this::toModel) + .collect(Collectors.toSet())); + } return model; } + + private OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) { + return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified()); + } + + private boolean validateDomainRepresentation(OrganizationDomainRepresentation rep) { + return rep != null && rep.getName() != null && !rep.getName().trim().isEmpty(); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java index 7e4a1a1e7a..321c789eaa 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertNotNull; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import org.keycloak.admin.client.resource.OrganizationResource; +import org.keycloak.representations.idm.OrganizationDomainRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.admin.AbstractAdminTest; @@ -43,12 +44,23 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { } protected OrganizationRepresentation createOrganization(String name) { + return createOrganization(name, null); + } + + protected OrganizationRepresentation createOrganization(String name, String orgDomain) { OrganizationRepresentation org = new OrganizationRepresentation(); org.setName(name); String id; + if (orgDomain != null) { + OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation(); + domainRep.setName(orgDomain); + domainRep.setVerified(true); + org.addDomain(domainRep); + } + try (Response response = testRealm().organizations().create(org)) { assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); id = ApiUtil.getCreatedId(response); 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 bdc75970eb..961dcf3c36 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.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.assertTrue; import static org.junit.Assert.fail; import java.util.ArrayList; @@ -35,6 +36,7 @@ import org.hamcrest.Matchers; import org.junit.Test; import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.common.Profile.Feature; +import org.keycloak.representations.idm.OrganizationDomainRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; @@ -48,6 +50,12 @@ public class OrganizationTest extends AbstractOrganizationTest { assertEquals(organizationName, expected.getName()); expected.setName("acme"); + // add an internet domain to the organization. + OrganizationDomainRepresentation orgDomain = new OrganizationDomainRepresentation(); + orgDomain.setName("neworg.org"); + orgDomain.setVerified(true); + expected.addDomain(orgDomain); + OrganizationResource organization = testRealm().organizations().get(expected.getId()); try (Response response = organization.update(expected)) { @@ -57,6 +65,49 @@ public class OrganizationTest extends AbstractOrganizationTest { OrganizationRepresentation existing = organization.toRepresentation(); assertEquals(expected.getId(), existing.getId()); assertEquals(expected.getName(), existing.getName()); + assertEquals(1, existing.getDomains().size()); + + OrganizationDomainRepresentation existingDomain = existing.getDomains().iterator().next(); + assertEquals(orgDomain.getName(), existingDomain.getName()); + assertEquals(orgDomain.isVerified(), existingDomain.isVerified()); + + // now test updating an existing internet domain (change verified to false and check the model was updated). + orgDomain.setVerified(false); + try (Response response = organization.update(expected)) { + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + existing = organization.toRepresentation(); + assertEquals(1, existing.getDomains().size()); + existingDomain = existing.getDomains().iterator().next(); + assertEquals(false, existingDomain.isVerified()); + + // now replace the internet domain for a different one. + orgDomain.setName("acme.com"); + try (Response response = organization.update(expected)) { + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + existing = organization.toRepresentation(); + assertEquals(1, existing.getDomains().size()); + existingDomain = existing.getDomains().iterator().next(); + assertEquals("acme.com", existingDomain.getName()); + assertEquals(false, existingDomain.isVerified()); + + // create another org and attempt to set the same internet domain during update - should not be possible. + OrganizationRepresentation anotherOrg = createOrganization("another-org"); + anotherOrg.addDomain(orgDomain); + + organization = testRealm().organizations().get(anotherOrg.getId()); + try (Response response = organization.update(anotherOrg)) { + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + // finally, attempt to create a new org with an existing internet domain in the representation - should not be possible. + OrganizationRepresentation newOrg = new OrganizationRepresentation(); + newOrg.setName("new-org"); + newOrg.addDomain(orgDomain); + try (Response response = testRealm().organizations().create(newOrg)) { + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } } @Test @@ -76,11 +127,33 @@ public class OrganizationTest extends AbstractOrganizationTest { expected.add(createOrganization("kc.org." + i)); } - List existing = testRealm().organizations().getAll(); + List existing = testRealm().organizations().getAll(null); assertFalse(existing.isEmpty()); MatcherAssert.assertThat(expected, Matchers.containsInAnyOrder(existing.toArray())); } + @Test + public void testGetByDomain() { + // create some organizations with a domain already set. + for (int i = 0; i < 5; i++) { + createOrganization("test-org-" + i, "testorg" + i + ".org"); + } + + // search for an organization with an existing domain. + List existing = testRealm().organizations().getAll("testorg2.org"); + assertEquals(1, existing.size()); + OrganizationRepresentation orgRep = existing.get(0); + assertEquals("test-org-2", orgRep.getName()); + assertEquals(1, orgRep.getDomains().size()); + OrganizationDomainRepresentation domainRep = orgRep.getDomains().iterator().next(); + assertEquals("testorg2.org", domainRep.getName()); + assertTrue(domainRep.isVerified()); + + // search for an organization with an non-existent domain. + existing = testRealm().organizations().getAll("someother.org"); + assertEquals(0, existing.size()); + } + @Test public void testDelete() { OrganizationRepresentation expected = createOrganization();