check-domain-setup-permission #97

Merged
hsh-michaelhoennig merged 17 commits from check-domain-setup-permission into master 2024-09-10 13:15:03 +02:00
3 changed files with 139 additions and 44 deletions
Showing only changes of commit 888e53397d - Show all commits

View File

@ -13,6 +13,7 @@ import java.util.HashMap;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
@ -39,6 +40,14 @@ public class Dns {
public record Result(Status status, List<String> records, NamingException exception) { 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) { public static Result fromRecords(final NamingEnumeration<?> recordEnumeration) {
final List<String> records = recordEnumeration == null final List<String> records = recordEnumeration == null
? emptyList() ? emptyList()
@ -52,10 +61,10 @@ public class Dns {
public static Result fromException(final NamingException exception) { public static Result fromException(final NamingException exception) {
return switch (exception) { return switch (exception) {
case ServiceUnavailableException exc -> new Result(Status.SERVICE_UNAVAILABLE, null, exc); case ServiceUnavailableException exc -> new Result(Status.SERVICE_UNAVAILABLE, emptyList(), exc);
case NameNotFoundException exc -> new Result(Status.NAME_NOT_FOUND, null, exc); case NameNotFoundException exc -> new Result(Status.NAME_NOT_FOUND, emptyList(), exc);
case InvalidNameException exc -> new Result(Status.INVALID_NAME, null, exc); case InvalidNameException exc -> new Result(Status.INVALID_NAME, emptyList(), exc);
case NamingException exc -> new Result(Status.UNKNOWN_FAILURE, null, exc); case NamingException exc -> new Result(Status.UNKNOWN_FAILURE, emptyList(), exc);
}; };
} }
} }

View File

@ -4,9 +4,11 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; 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.HsDomainHttpSetupHostingAssetValidator.SUBDOMAIN_NAME_REGEX; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainHttpSetupHostingAssetValidator.SUBDOMAIN_NAME_REGEX;
class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
@ -15,45 +17,55 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName"; public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
HsDomainSetupHostingAssetValidator() { HsDomainSetupHostingAssetValidator() {
super( DOMAIN_SETUP, super(
DOMAIN_SETUP,
AlarmContact.isOptional(), AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES); NO_EXTRA_PROPERTIES);
} }
// @Override // @Override
// public List<String> validateEntity(final HsHostingAsset assetEntity) { // public List<String> validateEntity(final HsHostingAsset assetEntity) {
// // TODO.impl: for newly created entities, check the permission of setting up a domain // // TODO.impl: for newly created entities, check the permission of setting up a domain
// // // //
// // allow if // // allow if
// // - user has Admin/Agent-role for all its sub-domains and the direct parent-Domain which are set up at at Hostsharing // // - 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 // // - domain has DNS zone with TXT record approval
// // - parent-domain has DNS zone with TXT record approval // // - parent-domain has DNS zone with TXT record approval
// // // //
// // TXT-Record check: // // TXT-Record check:
// // new InitialDirContext().getAttributes("dns:_netblocks.google.com", new String[] { "TXT"}).get("TXT").getAll(); // // new InitialDirContext().getAttributes("dns:_netblocks.google.com", new String[] { "TXT"}).get("TXT").getAll();
// final var violations = new ArrayList<String>(); // final var violations = new ArrayList<String>();
// if ( assetEntity.getBookingItem() != null ) { // if ( assetEntity.getBookingItem() != null ) {
// final var bookingItemDomainName = assetEntity .getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); // final var bookingItemDomainName = assetEntity .getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
// if ( bookingItemDomainName ) { // if ( bookingItemDomainName ) {
// violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name"); // violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name");
// } // }
// } // }
// violations.addAll(super.validateEntity(assetEntity)); // violations.addAll(super.validateEntity(assetEntity));
// return violations; // return violations;
// } // }
@Override @Override
public List<String> validateEntity(final HsHostingAsset assetEntity) { public List<String> validateEntity(final HsHostingAsset assetEntity) {
final var violations = new ArrayList<String>(); final var violations = new ArrayList<String>();
final var result = new Dns(assetEntity.getIdentifier()).fetchRecordsOfType("TXT"); final var domainName = assetEntity.getIdentifier();
switch ( result.status() ) { final var dnsResult = new Dns(domainName).fetchRecordsOfType("TXT");
switch (dnsResult.status()) {
hsh-michaelhoennig marked this conversation as resolved
Review

use getParentAsset == null + Test

use getParentAsset == null + Test
case Dns.Status.SUCCESS: case Dns.Status.SUCCESS:
final var code = assetEntity.getBookingItem().getDirectValue("verificationCode", String.class); final var code = assetEntity.getBookingItem().getDirectValue("verificationCode", String.class);
final var found = result.records().stream().filter(r -> r.contains("Hostsharing-domain-setup-verification-code=" + code)).findAny(); final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + code;
final var found = findTxtRecord(dnsResult, expectedTxtRecordValue)
.or(() -> superDomain(domainName)
.flatMap(superDomainName -> findTxtRecord(
new Dns(superDomainName).fetchRecordsOfType("TXT"),
expectedTxtRecordValue))
);
if (found.isEmpty()) { if (found.isEmpty()) {
violations.add("[DNS] no TXT record 'Hostsharing-domain-setup-verification="+code+"' with valid hash found for domain name '" + assetEntity.getIdentifier() + "'"); violations.add(
"[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + code + "' found for domain name '"
+ assetEntity.getIdentifier() + "'");
} }
break; break;
@ -65,22 +77,28 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'"); violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'");
break; break;
case Dns.Status.SERVICE_UNAVAILABLE: case Dns.Status.SERVICE_UNAVAILABLE:
case Dns.Status.UNKNOWN_FAILURE: case Dns.Status.UNKNOWN_FAILURE:
violations.add("[DNS] lookup failed for domain name '" + assetEntity.getIdentifier() + "': " + result.exception()); violations.add("[DNS] lookup failed for domain name '" + assetEntity.getIdentifier() + "': " + dnsResult.exception());
break; break;
} }
violations.addAll(super.validateEntity(assetEntity)); violations.addAll(super.validateEntity(assetEntity));
return violations; return violations;
} }
private static Optional<String> findTxtRecord(final Dns.Result result, final String expectedTxtRecordValue) {
return result.records().stream()
.filter(r -> r.contains(expectedTxtRecordValue))
.findAny();
}
@Override @Override
protected Pattern identifierPattern(final HsHostingAsset assetEntity) { protected Pattern identifierPattern(final HsHostingAsset assetEntity) {
if ( assetEntity.getBookingItem() != null ) { if (assetEntity.getBookingItem() != null) {
final var bookingItemDomainName = assetEntity.getBookingItem().getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); final var bookingItemDomainName = assetEntity.getBookingItem()
return Pattern.compile(bookingItemDomainName, Pattern.CASE_INSENSITIVE|Pattern.LITERAL); .getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
return Pattern.compile(bookingItemDomainName, Pattern.CASE_INSENSITIVE | Pattern.LITERAL);
} }
final var parentDomainName = assetEntity.getParentAsset().getIdentifier(); final var parentDomainName = assetEntity.getParentAsset().getIdentifier();
return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE); return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE);

View File

@ -76,7 +76,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
// then // then
assertThat(result).contains( assertThat(result).contains(
"'identifier' expected to match 'example.org', but is '"+testCase.domainName+"'" "'identifier' expected to match 'example.org', but is '" + testCase.domainName + "'"
); );
} }
@ -154,7 +154,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
} }
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = {"not-matching-booking-item-domain-name.org", "indirect.subdomain.example.org"}) @ValueSource(strings = { "not-matching-booking-item-domain-name.org", "indirect.subdomain.example.org" })
void rejectsDomainNameWhichIsNotADirectSubdomainOfParentAsset(final String newDomainName) { void rejectsDomainNameWhichIsNotADirectSubdomainOfParentAsset(final String newDomainName) {
// given // given
final var domainSetupHostingAssetEntity = validEntityBuilder() final var domainSetupHostingAssetEntity = validEntityBuilder()
@ -236,7 +236,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
} }
@Test @Test
void allowSetupOfNonExistingSubdomainOfRegistrarLevelDomain() { void allowSetupOfAvailableRegistrableDomain() {
// given // given
final var domainSetupHostingAssetEntity = validEntityBuilder().build(); final var domainSetupHostingAssetEntity = validEntityBuilder().build();
@ -254,12 +254,13 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
} }
@Test @Test
void rejectSetupOfExistingDomainWithInvalidDnsVerification() { void rejectSetupOfExistingRegistrableDomainWithoutValidDnsVerification() {
// given // given
final var domainSetupHostingAssetEntity = validEntityBuilder().build(); final var domainSetupHostingAssetEntity = validEntityBuilder().build();
final var domainName = domainSetupHostingAssetEntity.getIdentifier(); final var domainName = domainSetupHostingAssetEntity.getIdentifier();
final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationCode", String.class); final var expectedHash = domainSetupHostingAssetEntity.getBookingItem()
.getDirectValue("verificationCode", String.class);
Dns.fakeResultForDomain( Dns.fakeResultForDomain(
domainName, domainName,
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH")); Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH"));
@ -269,16 +270,18 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
final var result = validator.validateEntity(domainSetupHostingAssetEntity); final var result = validator.validateEntity(domainSetupHostingAssetEntity);
// then // then
assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + expectedHash + "' with valid hash found for domain name 'example.org'"); assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + expectedHash
+ "' found for domain name 'example.org'");
} }
@Test @Test
void allowSetupOfExistingDomainWithValidDnsVerification() { void allowSetupOfExistingRegistrableDomainWithValidDnsVerification() {
// given // given
final var domainSetupHostingAssetEntity = validEntityBuilder().build(); final var domainSetupHostingAssetEntity = validEntityBuilder().build();
final var domainName = domainSetupHostingAssetEntity.getIdentifier(); final var domainName = domainSetupHostingAssetEntity.getIdentifier();
final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationCode", String.class); final var expectedHash = domainSetupHostingAssetEntity.getBookingItem()
.getDirectValue("verificationCode", String.class);
Dns.fakeResultForDomain( Dns.fakeResultForDomain(
domainName, domainName,
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash)); Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
@ -291,6 +294,71 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
assertThat(result).isEmpty(); assertThat(result).isEmpty();
} }
@Test
void allowSetupOfUnregisteredSubdomainWithValidDnsVerificationInSuperDomain() {
// 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);
Dns.fakeResultForDomain(
"example.org",
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
// then
assertThat(result).isEmpty();
}
@Test
void allowSetupOfExistingSubdomainWithValidDnsVerificationInSuperDomain() {
// given
final var domainSetupHostingAssetEntity = validEntityBuilder("sub.example.org").build();
// ... the new subdomain is already registered:
Dns.fakeResultForDomain("sub.example.org", Dns.Result.fromRecords());
// ... and a valid verification-code in the super-domain:
final var expectedHash = domainSetupHostingAssetEntity.getBookingItem()
.getDirectValue("verificationCode", String.class);
Dns.fakeResultForDomain(
"example.org",
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
// then
assertThat(result).isEmpty();
}
@Test
void rejectSetupOfExistingSubdomainWithoutDnsVerification() {
// given
final var domainSetupHostingAssetEntity = validEntityBuilder("sub.example.org").build();
// ... the new subdomain is already registered:
Dns.fakeResultForDomain("sub.example.org", Dns.Result.fromRecords());
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).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + expectedHash
+ "' found for domain name 'sub.example.org'");
}
private static HsHostingAssetRealEntity createValidParentDomainSetupAsset(final String parentDomainName) { private static HsHostingAssetRealEntity createValidParentDomainSetupAsset(final String parentDomainName) {
final var bookingItem = HsBookingItemRealEntity.builder() final var bookingItem = HsBookingItemRealEntity.builder()
.type(HsBookingItemType.DOMAIN_SETUP) .type(HsBookingItemType.DOMAIN_SETUP)