Make organization domains optional

Closes #31285

Signed-off-by: Alexis Rico <sferadev@gmail.com>
This commit is contained in:
Alexis Rico
2025-06-16 08:45:57 +02:00
committed by Pedro Igor
parent 236d2f9f62
commit 224ccbb79d
5 changed files with 81 additions and 13 deletions

View File

@@ -47,7 +47,7 @@ Redirect URL::
After completing registration or accepting an invitation to the organization sent via email, the user is automatically redirected to the specified redirect url. If left empty, the user will be redirected to the account console by default.
Domains::
A set of one or more domains that belongs to this organization. A domain cannot be shared by different organizations within a realm.
A set of zero or more domains that belongs to this organization. A domain cannot be shared by different organizations within a realm. When no domains are specified, organization members will not be validated against domain restrictions during authentication and profile validation.
Description::
A free-text field to describe the organization.

View File

@@ -68,14 +68,12 @@ export const OrganizationForm = ({
fieldLabelId="domain"
/>
}
isRequired
>
<MultiLineInput
id="domain"
name="domains"
aria-label={t("domain")}
addButtonLabel="addDomain"
isRequired
/>
{errors?.["domains"]?.message && (
<FormErrorText message={errors["domains"].message.toString()} />

View File

@@ -42,6 +42,7 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.keycloak.connections.jpa.JpaConnectionProvider;
@@ -302,26 +303,28 @@ public class JpaOrganizationProvider implements OrganizationProvider {
private Predicate buildStringSearchPredicate(CriteriaBuilder builder, CriteriaQuery<?> query, Root<OrganizationEntity> org, String search,
Boolean exact) {
Root<OrganizationDomainEntity> domain = query.from(OrganizationDomainEntity.class);
List<Predicate> predicates = new ArrayList<>();
RealmModel realm = getRealm();
predicates.add(builder.equal(org.get("realmId"), realm.getId()));
predicates.add(builder.equal(org.get("id"), domain.get("organization").get("id")));
Predicate realmPredicate = builder.equal(org.get("realmId"), realm.getId());
if (StringUtil.isBlank(search)) {
return realmPredicate;
}
predicates.add(realmPredicate);
Join<OrganizationEntity, OrganizationDomainEntity> domain = org.join("domains", JoinType.LEFT);
Predicate namePredicate;
Predicate domainPredicate;
if (StringUtil.isBlank(search)) {
namePredicate = builder.conjunction(); // constant true
domainPredicate = builder.conjunction();
} else if (Boolean.TRUE.equals(exact)) {
if (Boolean.TRUE.equals(exact)) {
namePredicate = builder.equal(org.get("name"), search);
domainPredicate = builder.equal(domain.get("name"), search);
} else {
namePredicate = builder.like(builder.lower(org.get("name")), "%" + search.toLowerCase() + "%");
domainPredicate = builder.like(domain.get("name"), "%" + search.toLowerCase() + "%");
}
predicates.add(builder.or(namePredicate, domainPredicate));
return builder.and(predicates.toArray(new Predicate[0]));

View File

@@ -175,8 +175,8 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
@Override
public void setDomains(Set<OrganizationDomainModel> domains) {
if (domains == null || domains.isEmpty()) {
throw new ModelValidationException("You must provide at least one domain");
if (domains == null) {
return;
}
Map<String, OrganizationDomainModel> modelMap = domains.stream()

View File

@@ -21,6 +21,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
@@ -30,6 +32,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;
@@ -39,6 +42,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import jakarta.ws.rs.NotFoundException;
@@ -466,6 +470,69 @@ public class OrganizationTest extends AbstractOrganizationTest {
assertNotNull(existing.getDomain("acme.com"));
}
@Test
public void testWithoutDomains() {
// test create organization without any domains
OrganizationRepresentation orgWithoutDomains = new OrganizationRepresentation();
orgWithoutDomains.setName("no-domain-org");
orgWithoutDomains.setAlias("no-domain-org");
orgWithoutDomains.setDescription("Organization without domains");
String orgWithoutDomainsId;
try (Response response = testRealm().organizations().create(orgWithoutDomains)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
orgWithoutDomainsId = ApiUtil.getCreatedId(response);
}
OrganizationRepresentation created = testRealm().organizations().get(orgWithoutDomainsId).toRepresentation();
assertEquals("no-domain-org", created.getName());
assertEquals("no-domain-org", created.getAlias());
assertThat(created.getDomains() == null || created.getDomains().isEmpty(), is(true));
// verify that the organization can be retrieved
OrganizationRepresentation orgWithDomains = createRepresentation("org-with-domains", "example.com");
String orgWithDomainsId;
try (Response response = testRealm().organizations().create(orgWithDomains)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
orgWithDomainsId = ApiUtil.getCreatedId(response);
}
try {
List<OrganizationRepresentation> allOrgs = testRealm().organizations().list(-1, -1);
assertThat(allOrgs.size(), greaterThanOrEqualTo(2));
Optional<OrganizationRepresentation> foundOrgWithDomains = allOrgs.stream()
.filter(org -> org.getId().equals(orgWithDomainsId))
.findFirst();
Optional<OrganizationRepresentation> foundOrgWithoutDomains = allOrgs.stream()
.filter(org -> org.getId().equals(orgWithoutDomainsId))
.findFirst();
assertTrue("Organization with domains should be in the list", foundOrgWithDomains.isPresent());
assertTrue("Organization without domains should be in the list", foundOrgWithoutDomains.isPresent());
assertThat("Organization with domains should have domains",
foundOrgWithDomains.get().getDomains(), is(notNullValue()));
assertThat("Organization with domains should have at least one domain",
foundOrgWithDomains.get().getDomains().size(), greaterThan(0));
assertThat("Organization without domains should have no domains",
foundOrgWithoutDomains.get().getDomains() == null ||
foundOrgWithoutDomains.get().getDomains().isEmpty(), is(true));
List<OrganizationRepresentation> search = testRealm().organizations().search("with-domains", false, -1, -1);
assertThat(search, hasSize(1));
search = testRealm().organizations().search("no-domain", false, -1, -1);
assertThat(search, hasSize(1));
} finally {
testRealm().organizations().get(orgWithDomainsId).delete().close();
testRealm().organizations().get(orgWithoutDomainsId).delete().close();
}
}
@Test
public void testFilterEmptyDomain() {
//org should be created with only one domain