From c74c0be206d67cb297d73bc6df830583a5895978 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 6 Sep 2024 16:51:45 +0200 Subject: [PATCH] rejectSetupOfExistingDomainWithInvalidDnsVerification + allowSetupOfExistingDomainWithValidDnsVerification --- .../HsDomainSetupBookingItemValidator.java | 7 ++- .../hs/hosting/asset/validators/Dns.java | 16 ++++-- .../HsDomainSetupHostingAssetValidator.java | 8 +-- ...mainSetupBookingItemValidatorUnitTest.java | 3 +- ...ainSetupHostingAssetValidatorUnitTest.java | 53 +++++++++++++++++-- .../hs/migration/ImportHostingAssets.java | 2 +- 6 files changed, 75 insertions(+), 14 deletions(-) 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 30e04c0d..d540ca16 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 @@ -6,6 +6,8 @@ import net.hostsharing.hsadminng.mapper.Array; import java.util.ArrayList; import java.util.List; +import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_YESCRYPT; +import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { @@ -25,13 +27,16 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { "(co|ne|or|go|re|pe)\\.kr" ); public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName"; + public static final String VERIFICATION_PASSPHRASE_PROPERTY_NAME = "verificationPassphrase"; HsDomainSetupBookingItemValidator() { super( stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce() .matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name") .notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name") - .required() + .required(), + passwordProperty(VERIFICATION_PASSPHRASE_PROPERTY_NAME).minLength(8).maxLength(64) + .hashedUsing(LINUX_YESCRYPT).writeOnly().optional() ); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java index 353e2f5b..b99a0fc9 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java @@ -14,6 +14,7 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; +import static java.util.Arrays.stream; import static java.util.Collections.emptyList; public class Dns { @@ -38,13 +39,17 @@ public class Dns { public record Result(Status status, List records, NamingException exception) { - public static Result fromRecords(final NamingEnumeration enumeration) { - final List records = enumeration == null + public static Result fromRecords(final NamingEnumeration recordEnumeration) { + final List records = recordEnumeration == null ? emptyList() - : EnumerationUtils.toList(enumeration).stream().map(Object::toString).toList(); + : EnumerationUtils.toList(recordEnumeration).stream().map(Object::toString).toList(); return new Result(Status.SUCCESS, records, null); } + public static Result fromRecords(final String... records) { + return new Result(Status.SUCCESS, stream(records).toList(), null); + } + public static Result fromException(final NamingException exception) { return switch (exception) { case ServiceUnavailableException exc -> new Result(Status.SERVICE_UNAVAILABLE, null, exc); @@ -78,4 +83,9 @@ public class Dns { } } + public static void main(String[] args) { + final var result = new Dns("example.org").fetchRecordsOfType("TXT"); + System.out.println(result); + } + } 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 ce794398..2eb52144 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 @@ -50,10 +50,12 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { final var result = new Dns(assetEntity.getIdentifier()).fetchRecordsOfType("TXT"); switch ( result.status() ) { case Dns.Status.SUCCESS: - final var found = result.records().stream().filter(r -> r.contains("TXT Hostsharing-domain-setup-verification=FIXME")).findAny(); - if (found.isPresent()) { - break; + final var hash = assetEntity.getBookingItem().getDirectValue("verificationPassphrase", String.class); + final var found = result.records().stream().filter(r -> r.contains("Hostsharing-domain-setup-verification-code=" + hash)).findAny(); + if (found.isEmpty()) { + violations.add("[DNS] no TXT record 'Hostsharing-domain-setup-verification=...' with valid hash found for domain name '" + assetEntity.getIdentifier() + "'"); } + break; case Dns.Status.NAME_NOT_FOUND: // no DNS verification necessary / FIXME: at least if the superdomain is at registrar level 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 40f9d4cd..4b48f32a 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 @@ -129,6 +129,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(? validEntityBuilder(final String domainName) { final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) - .resources(Map.ofEntries( - Map.entry("domainName", domainName) - )) + .resources(new HashMap<>(ofEntries( + entry("domainName", domainName), + entry("verificationPassphrase", "some secret verification passphrase") + ))) .build(); + HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem); return HsHostingAssetRbacEntity.builder() .type(DOMAIN_SETUP) .bookingItem(bookingItem) @@ -248,11 +254,48 @@ class HsDomainSetupHostingAssetValidatorUnitTest { assertThat(result).isEmpty(); } + @Test + void rejectSetupOfExistingDomainWithInvalidDnsVerification() { + + // given + final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + final var domainName = domainSetupHostingAssetEntity.getIdentifier(); + Dns.fakeResultForDomain( + domainName, + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH")); + 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=...' with valid hash found for domain name 'example.org'"); + } + + @Test + void allowSetupOfExistingDomainWithValidDnsVerification() { + + // given + final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + final var domainName = domainSetupHostingAssetEntity.getIdentifier(); + final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationPassphrase", String.class); + Dns.fakeResultForDomain( + domainName, + 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(); + } + private static HsHostingAssetRealEntity createValidParentDomainSetupAsset(final String parentDomainName) { final var bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) - .resources(Map.ofEntries( - Map.entry("domainName", parentDomainName) + .resources(ofEntries( + entry("domainName", parentDomainName) )) .build(); final var parentAsset = HsHostingAssetRealEntity.builder() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java index 1a54605b..2f34ecee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java @@ -1352,7 +1352,7 @@ public class ImportHostingAssets extends BaseOfficeDataImport { } private void importDatabaseUsers(final String[] header, final List records) { - HashGenerator.enableChouldBeHash(true); + HashGenerator.enableCouldBeHash(true); final var columns = new Columns(header); records.stream() .map(this::trimAll)