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();