diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java index 664e7485..38234c07 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidator.java @@ -2,31 +2,18 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; -import net.hostsharing.hsadminng.mapper.Array; import jakarta.persistence.EntityManager; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(? fakeResults = new HashMap<>(); + public static Optional superDomain(final String domainName) { + final var parts = domainName.split("\\.", 2); + if (parts.length == 2) { + return Optional.of(parts[1]); + } + return Optional.empty(); + } + + public static boolean isRegistrarLevel(final String domainName) { + return stream(REGISTRAR_LEVEL_DOMAIN_PATTERN) + .anyMatch(p -> p.matcher(domainName).matches()); + } + public static void fakeResultForDomain(final String domainName, final Result fakeResult) { fakeResults.put(domainName, fakeResult); } @@ -40,13 +72,6 @@ public class Dns { public record Result(Status status, List records, NamingException exception) { - public static Optional superDomain(final String domainName) { - final var parts = domainName.split("\\.", 2); - if (parts.length == 2) { - return Optional.of(parts[1]); - } - return Optional.empty(); - } public static Result fromRecords(final NamingEnumeration recordEnumeration) { final List records = recordEnumeration == null diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java index 8a54f566..08ac5179 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java @@ -5,10 +5,11 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Supplier; import java.util.regex.Pattern; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.Result.superDomain; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.superDomain; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainHttpSetupHostingAssetValidator.SUBDOMAIN_NAME_REGEX; class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { @@ -24,54 +25,48 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { NO_EXTRA_PROPERTIES); } - // @Override - // public List validateEntity(final HsHostingAsset assetEntity) { - // // TODO.impl: for newly created entities, check the permission of setting up a domain - // // - // // allow if - // // - user has Admin/Agent-role for all its sub-domains and the direct parent-Domain which are set up at at Hostsharing - // // - domain has DNS zone with TXT record approval - // // - parent-domain has DNS zone with TXT record approval - // // - // // TXT-Record check: - // // new InitialDirContext().getAttributes("dns:_netblocks.google.com", new String[] { "TXT"}).get("TXT").getAll(); - // final var violations = new ArrayList(); - // if ( assetEntity.getBookingItem() != null ) { - // final var bookingItemDomainName = assetEntity .getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); - // if ( bookingItemDomainName ) { - // violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name"); - // } - // } - // violations.addAll(super.validateEntity(assetEntity)); - // return violations; - // } - @Override public List validateEntity(final HsHostingAsset assetEntity) { final var violations = new ArrayList(); final var domainName = assetEntity.getIdentifier(); final var dnsResult = new Dns(domainName).fetchRecordsOfType("TXT"); + final Supplier getCode = () -> assetEntity.getBookingItem().getDirectValue("verificationCode", String.class); switch (dnsResult.status()) { - case Dns.Status.SUCCESS: - final var code = assetEntity.getBookingItem().getDirectValue("verificationCode", String.class); - final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + code; - final var found = findTxtRecord(dnsResult, expectedTxtRecordValue) + case Dns.Status.SUCCESS: { + final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get(); + final var verificationFound = findTxtRecord(dnsResult, expectedTxtRecordValue) .or(() -> superDomain(domainName) .flatMap(superDomainName -> findTxtRecord( new Dns(superDomainName).fetchRecordsOfType("TXT"), expectedTxtRecordValue)) ); - if (found.isEmpty()) { + if (verificationFound.isEmpty()) { violations.add( - "[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + code + "' found for domain name '" - + assetEntity.getIdentifier() + "'"); + "[DNS] no TXT record '" + expectedTxtRecordValue + + "' found for domain name '" + domainName + "'"); } break; + } - case Dns.Status.NAME_NOT_FOUND: - // no DNS verification necessary / FIXME: at least if the superdomain is at registrar level + case Dns.Status.NAME_NOT_FOUND: { + final var superDomain = superDomain(domainName); + final var verificationRequired = !superDomain.map(Dns::isRegistrarLevel).orElse(false) + && assetEntity.getBookingItem() != null; // FIXME: or getParentAsset() == nuĺl? or extract method + if (verificationRequired) { + final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get(); + final var verificationFoundInSuperDomain = superDomain.flatMap(superDomainName -> findTxtRecord( + new Dns(superDomainName).fetchRecordsOfType("TXT"), + expectedTxtRecordValue)); + if (verificationFoundInSuperDomain.isEmpty()) { + violations.add( + "[DNS] no TXT record '" + expectedTxtRecordValue + + "' found for domain name '" + superDomain.orElseThrow() + "'"); + } + } + // otherwise no DNS verification to be able to setup DNS for domains to register break; + } case Dns.Status.INVALID_NAME: violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'"); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java index 1c7758cc..352c71de 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java @@ -26,14 +26,14 @@ class HsDomainSetupBookingItemValidatorUnitTest { private EntityManager em; @Test - void acceptsUnregisteredDomain() { + void acceptsRegisterableDomain() { // given final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() .type(DOMAIN_SETUP) .project(project) .caption("Test-Domain") .resources(Map.ofEntries( - entry("domainName", "example.org") // TODO.test: amend once we check registration + entry("domainName", "example.org") )) .build(); @@ -44,27 +44,9 @@ class HsDomainSetupBookingItemValidatorUnitTest { assertThat(result).isEmpty(); } - @Test - void rejectsTopLevelDomain() { - // given - final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() - .type(DOMAIN_SETUP) - .project(project) - .caption("Test-Domain") - .resources(Map.ofEntries( - entry("domainName", "org") - )) - .build(); - - // when - final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); - - // then - assertThat(result).containsExactly("'D-12345:Test-Project:Test-Domain.resources.domainName' = 'org' is not a (non-top-level) fully qualified domain name"); - } - @ParameterizedTest @ValueSource(strings = { + "de", "com", "net", "org", "actually-any-top-level-domain", "co.uk", "org.uk", "gov.uk", "ac.uk", "sch.uk", "com.au", "net.au", "org.au", "edu.au", "gov.au", "asn.au", "id.au", "co.jp", "ne.jp", "or.jp", "ac.jp", "go.jp", @@ -76,7 +58,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { "co.nz", "net.nz", "org.nz", "govt.nz", "ac.nz", "school.nz", "geek.nz", "kiwi.nz", "co.kr", "ne.kr", "or.kr", "go.kr", "re.kr", "pe.kr" }) - void reject2ndLevelRegistrarDomain(final String secondLevelRegistrarDomain) { + void rejectRegistrarLevelDomain(final String secondLevelRegistrarDomain) { // given final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() .type(DOMAIN_SETUP) @@ -91,7 +73,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); // then - assertThat(result).containsExactly( + assertThat(result).contains( "'D-12345:Test-Project:Test-Domain.resources.domainName' = '" + secondLevelRegistrarDomain + "' is a forbidden registrar-level domain name"); @@ -129,7 +111,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?