Add validation for the organization's internet domains.

Closes #28634

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2024-04-11 10:48:59 -03:00 committed by Pedro Igor
parent 0f88753f34
commit 2ab8bf852d
4 changed files with 27 additions and 7 deletions

View file

@ -23,7 +23,6 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -36,6 +35,7 @@ import org.keycloak.models.jpa.JpaModel;
import org.keycloak.models.jpa.entities.OrganizationDomainEntity; import org.keycloak.models.jpa.entities.OrganizationDomainEntity;
import org.keycloak.models.jpa.entities.OrganizationEntity; import org.keycloak.models.jpa.entities.OrganizationEntity;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
import org.keycloak.utils.EmailValidationUtil;
import java.util.List; import java.util.List;
@ -103,7 +103,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
} }
Map<String, OrganizationDomainModel> modelMap = domains.stream() Map<String, OrganizationDomainModel> modelMap = domains.stream()
.peek(this::isDomainInUse) .peek(this::validateDomainRepresentation)
.collect(Collectors.toMap(OrganizationDomainModel::getName, Function.identity())); .collect(Collectors.toMap(OrganizationDomainModel::getName, Function.identity()));
for (OrganizationDomainEntity domainEntity : new HashSet<>(this.entity.getDomains())) { for (OrganizationDomainEntity domainEntity : new HashSet<>(this.entity.getDomains())) {
@ -153,10 +153,23 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
return new OrganizationDomainModel(entity.getName(), entity.isVerified()); return new OrganizationDomainModel(entity.getName(), entity.isVerified());
} }
private void isDomainInUse(OrganizationDomainModel domainRep) { /**
OrganizationModel orgModel = provider.getByDomainName(domainRep.getName()); * Validates the domain representation. Specifically, the method first checks if the specified domain is valid,
* and then checks if the domain is not already linked to a different organization.
*
* @param domainModel the {@link OrganizationDomainModel} representing the domain being added.
* @throws {@link ModelValidationException} if the domain is invalid or is already linked to a different organization.
*/
private void validateDomainRepresentation(OrganizationDomainModel domainModel) {
String domainName = domainModel.getName();
// we rely on the same validation util used by the EmailValidator to ensure the domain part is consistently validated.
if(domainName == null || domainName.isEmpty() || !EmailValidationUtil.isValidEmail("nouser@" + domainName)) {
throw new ModelValidationException("The specified domain is invalid: " + domainName);
}
OrganizationModel orgModel = provider.getByDomainName(domainName);
if (orgModel != null && !Objects.equals(getId(), orgModel.getId())) { if (orgModel != null && !Objects.equals(getId(), orgModel.getId())) {
throw new ModelValidationException("Domain " + domainRep.getName() + " is already linked to another organization"); throw new ModelValidationException("Domain " + domainName + " is already linked to another organization");
} }
} }

View file

@ -43,7 +43,7 @@ public class OrganizationDomainModel implements Serializable {
} }
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name != null ? name.trim() : null;
} }
public boolean getVerified() { public boolean getVerified() {

View file

@ -187,6 +187,7 @@ public class OrganizationResource {
model.setName(rep.getName()); model.setName(rep.getName());
model.setAttributes(rep.getAttributes()); model.setAttributes(rep.getAttributes());
model.setDomains(Optional.ofNullable(rep.getDomains()).orElse(Set.of()).stream() model.setDomains(Optional.ofNullable(rep.getDomains()).orElse(Set.of()).stream()
.filter(Objects::nonNull)
.map(this::toModel) .map(this::toModel)
.collect(Collectors.toSet())); .collect(Collectors.toSet()));

View file

@ -23,7 +23,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.util.ArrayList; import java.util.ArrayList;
@ -212,6 +211,13 @@ public class OrganizationTest extends AbstractOrganizationTest {
assertEquals(expectedNewOrgBrDomain.getName(), existingNewOrgBrDomain.getName()); assertEquals(expectedNewOrgBrDomain.getName(), existingNewOrgBrDomain.getName());
assertEquals(expectedNewOrgBrDomain.isVerified(), existingNewOrgBrDomain.isVerified()); assertEquals(expectedNewOrgBrDomain.isVerified(), existingNewOrgBrDomain.isVerified());
// attempt to set the internet domain to an invalid domain.
expectedNewOrgBrDomain.setName("_invalid.domain.3com");
try (Response response = organization.update(expected)) {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
}
expectedNewOrgBrDomain.setName("acme.com");
// create another org and attempt to set the same internet domain during update - should not be possible. // create another org and attempt to set the same internet domain during update - should not be possible.
OrganizationRepresentation anotherOrg = createOrganization("another-org"); OrganizationRepresentation anotherOrg = createOrganization("another-org");
anotherOrg.addDomain(expectedNewOrgDomain); anotherOrg.addDomain(expectedNewOrgDomain);