no verification necessary directly for direct subdomains of registrar-level-domains if subdomain does not yet exist
This commit is contained in:
parent
888e53397d
commit
17a5aa2ff4
@ -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}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||
public static final String[] REGISTRAR_LEVEL_DOMAINS = Array.of(
|
||||
// "[^.]+", // top-level-domains are already rejected by FQDN_REGEX
|
||||
"(co|org|gov|ac|sch)\\.uk",
|
||||
"(com|net|org|edu|gov|asn|id)\\.au",
|
||||
"(co|ne|or|ac|go)\\.jp",
|
||||
"(com|net|org|gov|edu|ac)\\.cn",
|
||||
"(com|net|org|gov|edu|mil|art)\\.br",
|
||||
"(co|net|org|gen|firm|ind)\\.in",
|
||||
"(com|net|org|gob|edu)\\.mx",
|
||||
"(gov|edu)\\.it",
|
||||
"(co|net|org|govt|ac|school|geek|kiwi)\\.nz",
|
||||
"(co|ne|or|go|re|pe)\\.kr"
|
||||
);
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.mapper.Array;
|
||||
import org.apache.commons.collections4.EnumerationUtils;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
@ -14,14 +15,45 @@ import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public class Dns {
|
||||
|
||||
public static final String[] REGISTRAR_LEVEL_DOMAINS = Array.of(
|
||||
"[^.]+", // top-level-domains
|
||||
"(co|org|gov|ac|sch)\\.uk",
|
||||
"(com|net|org|edu|gov|asn|id)\\.au",
|
||||
"(co|ne|or|ac|go)\\.jp",
|
||||
"(com|net|org|gov|edu|ac)\\.cn",
|
||||
"(com|net|org|gov|edu|mil|art)\\.br",
|
||||
"(co|net|org|gen|firm|ind)\\.in",
|
||||
"(com|net|org|gob|edu)\\.mx",
|
||||
"(gov|edu)\\.it",
|
||||
"(co|net|org|govt|ac|school|geek|kiwi)\\.nz",
|
||||
"(co|ne|or|go|re|pe)\\.kr"
|
||||
);
|
||||
public static final Pattern[] REGISTRAR_LEVEL_DOMAIN_PATTERN = stream(REGISTRAR_LEVEL_DOMAINS)
|
||||
.map(Pattern::compile)
|
||||
.toArray(Pattern[]::new);
|
||||
|
||||
private final static Map<String, Result> fakeResults = new HashMap<>();
|
||||
|
||||
public static Optional<String> 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<String> records, NamingException exception) {
|
||||
|
||||
public static Optional<String> 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<String> records = recordEnumeration == null
|
||||
|
@ -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<String> 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<String>();
|
||||
// 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<String> validateEntity(final HsHostingAsset assetEntity) {
|
||||
|
||||
final var violations = new ArrayList<String>();
|
||||
final var domainName = assetEntity.getIdentifier();
|
||||
final var dnsResult = new Dns(domainName).fetchRecordsOfType("TXT");
|
||||
final Supplier<String> 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() + "'");
|
||||
|
@ -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}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[(co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, required=true, writeOnce=true}",
|
||||
"{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[[^.]+, (co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, required=true, writeOnce=true}",
|
||||
"{type=string, propertyName=verificationCode, readOnly=true, computed=IN_INIT}");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DnsUnitTest {
|
||||
|
||||
@Test
|
||||
void isRegistrarLevel() {
|
||||
assertThat(Dns.isRegistrarLevel("de")).isTrue();
|
||||
assertThat(Dns.isRegistrarLevel("example.de")).isFalse();
|
||||
|
||||
assertThat(Dns.isRegistrarLevel("co.uk")).isTrue();
|
||||
assertThat(Dns.isRegistrarLevel("example.co.uk")).isFalse();
|
||||
}
|
||||
}
|
@ -97,8 +97,12 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
@EnumSource(ValidDomainNameIdentifier.class)
|
||||
void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) {
|
||||
// given
|
||||
Dns.fakeResultForDomain(testCase.domainName, new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null));
|
||||
final var givenEntity = validEntityBuilder(testCase.domainName).identifier(testCase.domainName).build();
|
||||
final var expectedHash = givenEntity.getBookingItem()
|
||||
.getDirectValue("verificationCode", String.class);
|
||||
Dns.fakeResultForDomain(
|
||||
testCase.domainName,
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
|
||||
|
||||
// when
|
||||
@ -270,7 +274,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + expectedHash
|
||||
assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash
|
||||
+ "' found for domain name 'example.org'");
|
||||
}
|
||||
|
||||
@ -318,6 +322,29 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void RejectSetupOfUnregisteredSubdomainWithoutDnsVerificationInSuperDomain() {
|
||||
|
||||
// given
|
||||
final var domainSetupHostingAssetEntity = validEntityBuilder("sub.example.org").build();
|
||||
// ... the new subdomain is not yet registered:
|
||||
Dns.fakeResultForDomain(
|
||||
"sub.example.org",
|
||||
Dns.Result.fromException(new NameNotFoundException("domain not registered")));
|
||||
// ... and a valid verification-code in the super-domain:
|
||||
final var expectedHash = domainSetupHostingAssetEntity.getBookingItem()
|
||||
.getDirectValue("verificationCode", String.class);
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
|
||||
|
||||
// when
|
||||
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactly(
|
||||
"[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash
|
||||
+ "' found for domain name 'example.org'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowSetupOfExistingSubdomainWithValidDnsVerificationInSuperDomain() {
|
||||
|
||||
@ -355,7 +382,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + expectedHash
|
||||
assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash
|
||||
+ "' found for domain name 'sub.example.org'");
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user