From 9cbdb1fc47c1c0849fc3bc54feecb0ac9152326b Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 5 Sep 2024 11:05:06 +0200 Subject: [PATCH 01/16] disallow domain setup for top-level and registrar-level domains --- .../HsDomainSetupBookingItemValidator.java | 25 +++- .../hs/validation/StringProperty.java | 81 ++++++++++-- ...mainSetupBookingItemValidatorUnitTest.java | 117 ++++++++++++++++++ ...ttpSetupHostingAssetValidatorUnitTest.java | 8 +- ...lAddressHostingAssetValidatorUnitTest.java | 4 +- ...UnixUserHostingAssetValidatorUnitTest.java | 2 +- 6 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java 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 a48ed4a5..f3062fc2 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 @@ -1,10 +1,33 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.mapper.Array; + +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}(?> extends ValidatableProp protected static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, - Array.of("matchesRegEx", "minLength", "maxLength", "provided"), + Array.of("matchesRegEx", "notMatchesRegEx", "minLength", "maxLength", "provided"), ValidatableProperty.KEY_ORDER_TAIL, Array.of("undisclosed")); private String[] provided; private Pattern[] matchesRegEx; + private String matchesRegExDescription; + private Pattern[] notMatchesRegEx; + private String notMatchesRegExDescription; + @Setter(AccessLevel.PRIVATE) + private Consumer describedAsConsumer; private Integer minLength; private Integer maxLength; private boolean undisclosed; @@ -56,10 +63,23 @@ public class StringProperty

> extends ValidatableProp public P matchesRegEx(final String... regExPattern) { this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new); + this.describedAsConsumer = violationMessage -> matchesRegExDescription = violationMessage; return self(); } - /// predifined values, similar to fixed values in a combobox + public P notMatchesRegEx(final String... regExPattern) { + this.notMatchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new); + this.describedAsConsumer = violationMessage -> notMatchesRegExDescription = violationMessage; + return self(); + } + + public P describedAs(final String violationMessage) { + describedAsConsumer.accept(violationMessage); + describedAsConsumer = null; + return self(); + } + + /// predefined values, similar to fixed values in a combobox public P provided(final String... provided) { this.provided = provided; return self(); @@ -78,16 +98,10 @@ public class StringProperty

> extends ValidatableProp @Override protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { super.validate(result, propValue, propProvider); - if (minLength != null && propValue.length()maxLength) { - result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length()); - } - if (matchesRegEx != null && - stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) { - result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match" + (matchesRegEx.length>1?" any":"")); - } + validateMinLength(result, propValue); + validateMaxLength(result, propValue); + validateMatchesRegEx(result, propValue); + validateNotMatchesRegEx(result, propValue); } @Override @@ -99,4 +113,47 @@ public class StringProperty

> extends ValidatableProp protected String simpleTypeName() { return "string"; } + + private void validateMinLength(final List result, final String propValue) { + if (minLength != null && propValue.length() result, final String propValue) { + if (maxLength != null && propValue.length()>maxLength) { + result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length()); + } + } + + private void validateMatchesRegEx(final List result, final String propValue) { + if (matchesRegEx != null && + stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) { + if (matchesRegExDescription != null) { + result.add(propertyName + "' = " + display(propValue) + " " + matchesRegExDescription); + } else if (matchesRegEx.length>1) { + result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + + " but " + display(propValue) + " does not match any"); + } else { + result.add(propertyName + "' is expected to match " + Arrays.toString(matchesRegEx) + " but " + display( + propValue) + + " does not match"); + } + } + } + + private void validateNotMatchesRegEx(final List result, final String propValue) { + if (notMatchesRegEx != null && + stream(notMatchesRegEx).map(p -> p.matcher(propValue)).anyMatch(Matcher::matches)) { + if (notMatchesRegExDescription != null) { + result.add(propertyName + "' = " + display(propValue) + " " + notMatchesRegExDescription); + } else if (notMatchesRegEx.length>1) { + result.add(propertyName + "' is expected not to match any of " + Arrays.toString(notMatchesRegEx) + + " but " + display(propValue) + " does match at least one"); + } else { + result.add(propertyName + "' is expected not to match " + Arrays.toString(notMatchesRegEx) + + " but " + display(propValue) + " does match"); + } + } + } } 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 new file mode 100644 index 00000000..2a83f418 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsDomainSetupBookingItemValidatorUnitTest.java @@ -0,0 +1,117 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import jakarta.persistence.EntityManager; +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainSetupBookingItemValidatorUnitTest { + + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectRealEntity project = HsBookingProjectRealEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + private EntityManager em; + + @Test + void acceptsUnregisteredDomain() { + // given + final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", "example.org") // TODO.test: amend once we check registration + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsTopLevelDomain() { + // given + final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", "org") + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + + // 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 = { + "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", + "com.cn", "net.cn", "org.cn", "gov.cn", "edu.cn", "ac.cn", + "com.br", "net.br", "org.br", "gov.br", "edu.br", "mil.br", "art.br", + "co.in", "net.in", "org.in", "gen.in", "firm.in", "ind.in", + "com.mx", "net.mx", "org.mx", "gob.mx", "edu.mx", + "gov.it", "edu.it", + "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) { + // given + final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", secondLevelRegistrarDomain) + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + + // then + assertThat(result).containsExactly( + "'D-12345:Test-Project:Test-Domain.resources.domainName' = '" + + secondLevelRegistrarDomain + + "' is a forbidden registrar-level domain name"); + } + + @Test + void containsAllValidations() { + // when + final var validator = HsBookingItemEntityValidatorRegistry.forType(CLOUD_SERVER); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=boolean, propertyName=active, defaultValue=true}", + "{type=integer, propertyName=CPU, min=1, max=32, required=true}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=8192, required=true}", + "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, requiresAtLeastOneOf=[SDD, HDD]}", + "{type=integer, propertyName=HDD, unit=GB, min=250, max=4000, step=250, requiresAtLeastOneOf=[SSD, HDD]}", + "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}", + "{type=integer, propertyName=Bandwidth, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}", + "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java index 4705a99e..91fecdd5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java @@ -156,9 +156,9 @@ class HsDomainHttpSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.htdocsfallback' is expected to be of type Boolean, but is of type String", - "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.fcgi-php-bin' is expected to match any of [^/.*] but 'false' does not match", - "'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match any of [(\\*|(?!-)[A-Za-z0-9-]{1,63}(? Date: Thu, 5 Sep 2024 11:35:20 +0200 Subject: [PATCH 02/16] set BI domainName in import and fix validators info string test --- .../hsadminng/hs/validation/StringProperty.java | 5 ++++- .../HsDomainSetupBookingItemValidatorUnitTest.java | 11 ++--------- .../hsadminng/hs/migration/ImportHostingAssets.java | 2 ++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index 601f12b6..6dc463d6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -17,7 +17,10 @@ public class StringProperty

> extends ValidatableProp protected static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, - Array.of("matchesRegEx", "notMatchesRegEx", "minLength", "maxLength", "provided"), + Array.of("matchesRegEx", "matchesRegExDescription", + "notMatchesRegEx", "notMatchesRegExDescription", + "minLength", "maxLength", + "provided"), ValidatableProperty.KEY_ORDER_TAIL, Array.of("undisclosed")); private String[] provided; 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 2a83f418..7305967b 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 @@ -101,17 +101,10 @@ class HsDomainSetupBookingItemValidatorUnitTest { @Test void containsAllValidations() { // when - final var validator = HsBookingItemEntityValidatorRegistry.forType(CLOUD_SERVER); + final var validator = HsBookingItemEntityValidatorRegistry.forType(DOMAIN_SETUP); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=boolean, propertyName=active, defaultValue=true}", - "{type=integer, propertyName=CPU, min=1, max=32, required=true}", - "{type=integer, propertyName=RAM, unit=GB, min=1, max=8192, required=true}", - "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, requiresAtLeastOneOf=[SDD, HDD]}", - "{type=integer, propertyName=HDD, unit=GB, min=250, max=4000, step=250, requiresAtLeastOneOf=[SSD, HDD]}", - "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}", - "{type=integer, propertyName=Bandwidth, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}", - "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); + "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(? Date: Thu, 5 Sep 2024 12:07:05 +0200 Subject: [PATCH 03/16] disallow to set up Hostsharing-Domain-Names --- .../HsDomainSetupBookingItemValidator.java | 24 ++++++++++-- .../HsDomainSetupHostingAssetValidator.java | 14 ------- ...mainSetupBookingItemValidatorUnitTest.java | 38 +++++++++++++++---- ...sHostingAssetControllerAcceptanceTest.java | 2 + 4 files changed, 53 insertions(+), 25 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 f3062fc2..30e04c0d 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 @@ -1,13 +1,17 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.mapper.Array; +import java.util.ArrayList; +import java.util.List; + 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}(? validateEntity(final HsBookingItem bookingItem) { + final var violations = new ArrayList(); + final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); + if ( !bookingItem.isLoaded() && + domainName.matches("hostsharing.(com|net|org|coop)") ) { + violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name"); + } + violations.addAll(super.validateEntity(bookingItem)); + return violations; + } } 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 8701d2fe..ea306453 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 @@ -25,20 +25,6 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { public List validateEntity(final HsHostingAsset assetEntity) { // TODO.impl: for newly created entities, check the permission of setting up a domain // - // reject, if the domain is any of these: - // hostsharing.com|net|org|coop, // just to be on the safe side - // [^.}+, // 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, - // com.cn, net.cn, org.cn, gov.cn, edu.cn, ac.cn, - // com.br, net.br, org.br, gov.br, edu.br, mil.br, art.br, - // co.in, net.in, org.in, gen.in, firm.in, ind.in, - // com.mx, net.mx, org.mx, gob.mx, edu.mx, - // gov.it, edu.it, - // 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 - // // 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 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 7305967b..40f9d4cd 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 @@ -11,7 +11,6 @@ import jakarta.persistence.EntityManager; import java.util.Map; import static java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP; import static org.assertj.core.api.Assertions.assertThat; @@ -29,7 +28,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { @Test void acceptsUnregisteredDomain() { // given - final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() .type(DOMAIN_SETUP) .project(project) .caption("Test-Domain") @@ -39,7 +38,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .build(); // when - final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); // then assertThat(result).isEmpty(); @@ -48,7 +47,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { @Test void rejectsTopLevelDomain() { // given - final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() .type(DOMAIN_SETUP) .project(project) .caption("Test-Domain") @@ -58,7 +57,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .build(); // when - final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + 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"); @@ -79,7 +78,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { }) void reject2ndLevelRegistrarDomain(final String secondLevelRegistrarDomain) { // given - final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder() + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() .type(DOMAIN_SETUP) .project(project) .caption("Test-Domain") @@ -89,7 +88,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { .build(); // when - final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); // then assertThat(result).containsExactly( @@ -98,6 +97,31 @@ class HsDomainSetupBookingItemValidatorUnitTest { "' is a forbidden registrar-level domain name"); } + @ParameterizedTest + @ValueSource(strings = { + "hostsharing.net", "hostsharing.org", "hostsharing.com", "hostsharing.coop" + }) + void rejectHostsharingDomain(final String secondLevelRegistrarDomain) { + // given + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", secondLevelRegistrarDomain) + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); + + // then + assertThat(result).containsExactly( + "'D-12345:Test-Project:Test-Domain.resources.domainName' = '" + + secondLevelRegistrarDomain + + "' is a forbidden Hostsharing domain name"); + } + @Test void containsAllValidations() { // when diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 306337bb..8b04bbb0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -256,6 +256,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .project(givenProject) .type(HsBookingItemType.DOMAIN_SETUP) .caption("some temp domain setup booking item") + .resources(Map.ofEntries( + entry("domainName", "example.org"))) .build() ); -- 2.39.5 From 0ed7264fc1d0059e9c490b19497c5807623aa39c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 5 Sep 2024 13:22:20 +0200 Subject: [PATCH 04/16] check hostingasset domain-setup domain name against booking item and parent asset --- .../HsDomainSetupHostingAssetValidator.java | 48 +++++++++------ ...ainSetupHostingAssetValidatorUnitTest.java | 59 ++++++++++++++++++- 2 files changed, 87 insertions(+), 20 deletions(-) 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 ea306453..c9623dd2 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 @@ -2,42 +2,52 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; -import java.util.List; 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.HsDomainHttpSetupHostingAssetValidator.SUBDOMAIN_NAME_REGEX; class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(? 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(); - - return super.validateEntity(assetEntity); - } +// @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 protected Pattern identifierPattern(final HsHostingAsset assetEntity) { - return identifierPattern; + if ( assetEntity.getBookingItem() != null ) { + final var bookingItemDomainName = assetEntity.getBookingItem().getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); + return Pattern.compile(bookingItemDomainName, Pattern.CASE_INSENSITIVE|Pattern.LITERAL); + } + final var parentDomainName = assetEntity.getParentAsset().getIdentifier(); + return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index 6f451556..266bad45 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; import java.util.Map; @@ -18,9 +19,15 @@ import static org.assertj.core.api.Assertions.assertThat; class HsDomainSetupHostingAssetValidatorUnitTest { static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder() { + final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .resources(Map.ofEntries( + Map.entry("domainName", "example.org") + )) + .build(); return HsHostingAssetRbacEntity.builder() .type(DOMAIN_SETUP) - .bookingItem(HsBookingItemRealEntity.builder().type(HsBookingItemType.DOMAIN_SETUP).build()) + .bookingItem(bookingItem) .identifier("example.org"); } @@ -111,6 +118,42 @@ class HsDomainSetupHostingAssetValidatorUnitTest { "'DOMAIN_SETUP:example.org.assignedToAsset' must be null but is of type MANAGED_SERVER"); } + @Test + void rejectsDomainNameNotMatchingBookingItemDomainName() { + // given + final var domainSetupHostingAssetEntity = validEntityBuilder() + .identifier("not-matching-booking-item-domain-name.org") + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(domainSetupHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match 'example.org', but is 'not-matching-booking-item-domain-name.org'"); + } + + @ParameterizedTest + @ValueSource(strings = {"not-matching-booking-item-domain-name.org", "indirect.subdomain.example.org"}) + void rejectsDomainNameWhichIsNotADirectSubdomainOfParentAsset(final String newDomainName) { + // given + final var domainSetupHostingAssetEntity = validEntityBuilder() + .bookingItem(null) + .parentAsset(createValidParentDomainSetupAsset("example.org")) + .identifier(newDomainName) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(domainSetupHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match '(\\*|(?!-)[A-Za-z0-9-]{1,63}(? Date: Thu, 5 Sep 2024 13:51:08 +0200 Subject: [PATCH 05/16] amend test data+assertions to new validation rules --- .../HsHostingAssetControllerAcceptanceTest.java | 2 +- ...omainSetupHostingAssetValidatorUnitTest.java | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 8b04bbb0..ba224516 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -257,7 +257,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .type(HsBookingItemType.DOMAIN_SETUP) .caption("some temp domain setup booking item") .resources(Map.ofEntries( - entry("domainName", "example.org"))) + entry("domainName", "example.com"))) .build() ); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index 266bad45..876d92b7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -18,17 +18,22 @@ import static org.assertj.core.api.Assertions.assertThat; class HsDomainSetupHostingAssetValidatorUnitTest { - static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder() { + + static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder(final String domainName) { final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) .resources(Map.ofEntries( - Map.entry("domainName", "example.org") + Map.entry("domainName", domainName) )) .build(); return HsHostingAssetRbacEntity.builder() .type(DOMAIN_SETUP) .bookingItem(bookingItem) - .identifier("example.org"); + .identifier(domainName); + } + + static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder() { + return validEntityBuilder("example.org"); } enum InvalidDomainNameIdentifier { @@ -57,13 +62,13 @@ class HsDomainSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^((?!-)[A-Za-z0-9-]{1,63}(? Date: Thu, 5 Sep 2024 16:36:55 +0200 Subject: [PATCH 06/16] add DNS TXT record verification (WIP) --- .../hs/hosting/asset/validators/Dns.java | 74 +++++++++++++++++++ .../HsDomainSetupHostingAssetValidator.java | 36 +++++++++ ...sHostingAssetControllerAcceptanceTest.java | 2 + ...ainSetupHostingAssetValidatorUnitTest.java | 6 +- .../hs/migration/BaseOfficeDataImport.java | 2 +- 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java 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 new file mode 100644 index 00000000..1eb7366f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/Dns.java @@ -0,0 +1,74 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import org.apache.commons.collections4.EnumerationUtils; + +import javax.naming.InvalidNameException; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.ServiceUnavailableException; +import javax.naming.directory.Attribute; +import javax.naming.directory.InitialDirContext; +import java.util.Hashtable; +import java.util.List; + +import static java.util.Collections.emptyList; + +public class Dns { + + private static Result nextFakeResult = null; + + public static void fakeNextResult(final Result fakeResult) { + nextFakeResult = fakeResult; + } + + public enum Status { + SUCCESS, + RECORD_TYPE_NOT_FOUND, + NAME_NOT_FOUND, + INVALID_NAME, + SERVICE_UNAVAILABLE, + UNKNOWN_FAILURE + } + + public record Result(Status status, List records, NamingException exception) { + } + + private final String domainName; + + public Dns(final String domainName) { + this.domainName = domainName; + } + + public Result fetchRecordsOfType(final String recordType) { + if (nextFakeResult != null) { + try { + return nextFakeResult; + } finally { + nextFakeResult = null; + } + } + + try { + final var env = new Hashtable<>(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + final Attribute r = new InitialDirContext(env) + .getAttributes(domainName, new String[] { recordType }) + .get(recordType); + return new Result( + r == null ? Status.RECORD_TYPE_NOT_FOUND : Status.SUCCESS, + r == null + ? emptyList() + : EnumerationUtils.toList(r.getAll()).stream().map(Object::toString).toList(), + null); + } catch (final ServiceUnavailableException e) { + return new Result(Status.SERVICE_UNAVAILABLE, null, e); + } catch (final NameNotFoundException e) { + return new Result(Status.NAME_NOT_FOUND, null, e); + } catch (InvalidNameException e) { + return new Result(Status.INVALID_NAME, null, e); + } catch (NamingException e) { + return new Result(Status.UNKNOWN_FAILURE, null, e); + } + } + +} 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 c9623dd2..26eec252 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 @@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Pattern; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; @@ -41,6 +43,40 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { // return violations; // } + @Override + public List validateEntity(final HsHostingAsset assetEntity) { + + final var violations = new ArrayList(); + 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; + } + case Dns.Status.RECORD_TYPE_NOT_FOUND: + violations.add("Domain " + assetEntity.getIdentifier() + " exists, but no record 'TXT Hostsharing-domain-setup-challenge:FIXME' found "); + break; + + case Dns.Status.NAME_NOT_FOUND: + // no DNS verification necessary + break; + + case Dns.Status.INVALID_NAME: + violations.add("Invalid domain name " + assetEntity.getIdentifier()); + break; + + case Dns.Status.SERVICE_UNAVAILABLE: + case Dns.Status.UNKNOWN_FAILURE: + violations.add("DNS request for " + assetEntity.getIdentifier() + " failed: " + result.exception()); + break; + } + + + violations.addAll(super.validateEntity(assetEntity)); + return violations; + } + @Override protected Pattern identifierPattern(final HsHostingAsset assetEntity) { if ( assetEntity.getBookingItem() != null ) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index ba224516..1eaf5bbf 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -9,6 +9,7 @@ import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; @@ -249,6 +250,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddTopLevelAsset() { context.define("superuser-alex@hostsharing.net"); + Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var givenProject = realProjectRepo.findByCaption("D-1000111 default project").stream() .findAny().orElseThrow(); final var bookingItem = givenSomeTemporaryBookingItem(() -> diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index 876d92b7..baebdf40 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; class HsDomainSetupHostingAssetValidatorUnitTest { - static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder(final String domainName) { final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) @@ -54,6 +53,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @EnumSource(InvalidDomainNameIdentifier.class) void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) { // given + Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build(); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); @@ -84,6 +84,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @EnumSource(ValidDomainNameIdentifier.class) void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) { // given + Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var givenEntity = validEntityBuilder(testCase.domainName).identifier(testCase.domainName).build(); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); @@ -106,6 +107,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @Test void validatesReferencedEntities() { // given + Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var domainSetupHostingAssetEntity = validEntityBuilder() .parentAsset(HsHostingAssetRealEntity.builder().type(CLOUD_SERVER).build()) .assignedToAsset(HsHostingAssetRealEntity.builder().type(MANAGED_SERVER).build()) @@ -161,7 +163,9 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @Test void expectsEitherParentAssetOrBookingItem() { + // given + Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var domainSetupHostingAssetEntity = validEntityBuilder().build(); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java index 9cb774d2..f00d57dd 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/BaseOfficeDataImport.java @@ -69,7 +69,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport { 512167, // 11139, partner without contractual contact 512170, // 11142, partner without contractual contact 511725, // 10764, partner without contractual contact - // 512171, // 11143, partner without partner contact -- exc + // 512171, // 11143, partner without partner contact -- exception -1 ); -- 2.39.5 From bab85c558190dbb2c2db62c145f71747f9099ecd Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 6 Sep 2024 12:39:11 +0200 Subject: [PATCH 07/16] test for handlung DNS lookup failures --- .../hs/hosting/asset/validators/Dns.java | 37 ++++++++------- .../HsDomainSetupHostingAssetValidator.java | 5 +- ...ainSetupHostingAssetValidatorUnitTest.java | 47 +++++++++++++++++++ 3 files changed, 69 insertions(+), 20 deletions(-) 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 1eb7366f..63303110 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 @@ -4,6 +4,7 @@ import org.apache.commons.collections4.EnumerationUtils; import javax.naming.InvalidNameException; import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.ServiceUnavailableException; import javax.naming.directory.Attribute; @@ -23,7 +24,6 @@ public class Dns { public enum Status { SUCCESS, - RECORD_TYPE_NOT_FOUND, NAME_NOT_FOUND, INVALID_NAME, SERVICE_UNAVAILABLE, @@ -31,6 +31,22 @@ public class Dns { } public record Result(Status status, List records, NamingException exception) { + + public static Result fromRecords(final NamingEnumeration enumeration) { + final List records = enumeration == null + ? emptyList() + : EnumerationUtils.toList(enumeration).stream().map(Object::toString).toList(); + return new Result(Status.SUCCESS, records, null); + } + + public static Result fromException(final NamingException exception) { + return switch (exception) { + case ServiceUnavailableException exc -> new Result(Status.SERVICE_UNAVAILABLE, null, exc); + case NameNotFoundException exc -> new Result(Status.NAME_NOT_FOUND, null, exc); + case InvalidNameException exc -> new Result(Status.INVALID_NAME, null, exc); + case NamingException exc -> new Result(Status.UNKNOWN_FAILURE, null, exc); + }; + } } private final String domainName; @@ -51,23 +67,12 @@ public class Dns { try { final var env = new Hashtable<>(); env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); - final Attribute r = new InitialDirContext(env) + final Attribute records = new InitialDirContext(env) .getAttributes(domainName, new String[] { recordType }) .get(recordType); - return new Result( - r == null ? Status.RECORD_TYPE_NOT_FOUND : Status.SUCCESS, - r == null - ? emptyList() - : EnumerationUtils.toList(r.getAll()).stream().map(Object::toString).toList(), - null); - } catch (final ServiceUnavailableException e) { - return new Result(Status.SERVICE_UNAVAILABLE, null, e); - } catch (final NameNotFoundException e) { - return new Result(Status.NAME_NOT_FOUND, null, e); - } catch (InvalidNameException e) { - return new Result(Status.INVALID_NAME, null, e); - } catch (NamingException e) { - return new Result(Status.UNKNOWN_FAILURE, null, e); + return Result.fromRecords(records != null ? records.getAll() : null); + } catch (final NamingException exception) { + return Result.fromException(exception); } } 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 26eec252..afd4ca85 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 @@ -54,12 +54,9 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { if (found.isPresent()) { break; } - case Dns.Status.RECORD_TYPE_NOT_FOUND: - violations.add("Domain " + assetEntity.getIdentifier() + " exists, but no record 'TXT Hostsharing-domain-setup-challenge:FIXME' found "); - break; case Dns.Status.NAME_NOT_FOUND: - // no DNS verification necessary + // no DNS verification necessary / FIXME: at least if the superdomain is at registrar level break; case Dns.Status.INVALID_NAME: diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index baebdf40..867fd4f7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -9,6 +9,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; +import javax.naming.InvalidNameException; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.ServiceUnavailableException; import java.util.Map; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; @@ -176,6 +180,49 @@ class HsDomainSetupHostingAssetValidatorUnitTest { assertThat(result).isEmpty(); } + enum DnsLookupFailureTestCase { + SERVICE_UNAVAILABLE( + new ServiceUnavailableException("no Internet connection"), + "DNS request for example.org failed: javax.naming.ServiceUnavailableException: no Internet connection"), + NAME_NOT_FOUND( + new NameNotFoundException("domain not registered"), + null), // no + INVALID_NAME( + new InvalidNameException("domain name too long or whatever"), + "Invalid domain name example.org"), + UNKNOWN_FAILURE( + new NamingException("some other problem"), + "DNS request for example.org failed: javax.naming.NamingException: some other problem"); + + public final NamingException givenException; + public final String expectedErrorMessage; + + DnsLookupFailureTestCase(final NamingException givenException, final String expectedErrorMessage) { + this.givenException = givenException; + this.expectedErrorMessage = expectedErrorMessage; + } + } + + @ParameterizedTest + @EnumSource(DnsLookupFailureTestCase.class) + void handlesDnsLookupFailures(final DnsLookupFailureTestCase testCase) { + + // given + Dns.fakeNextResult(Dns.Result.fromException(testCase.givenException)); + final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(domainSetupHostingAssetEntity); + + // then + if (testCase.expectedErrorMessage != null) { + assertThat(result).containsExactly(testCase.expectedErrorMessage); + } else { + assertThat(result).isEmpty(); + } + } + private static HsHostingAssetRealEntity createValidParentDomainSetupAsset(final String parentDomainName) { final var bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) -- 2.39.5 From 8316a88bcea4df5bf613fb65af3c88e3ddf45306 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 6 Sep 2024 13:08:02 +0200 Subject: [PATCH 08/16] allowSetupOfNonExistingSubdomainOfRegistrarLevelDomain --- .../hs/hosting/asset/validators/Dns.java | 20 ++++---- .../HsDomainSetupHostingAssetValidator.java | 4 +- ...sHostingAssetControllerAcceptanceTest.java | 2 +- ...ainSetupHostingAssetValidatorUnitTest.java | 47 ++++++++++++++----- 4 files changed, 50 insertions(+), 23 deletions(-) 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 63303110..353e2f5b 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 @@ -9,17 +9,23 @@ import javax.naming.NamingException; import javax.naming.ServiceUnavailableException; import javax.naming.directory.Attribute; import javax.naming.directory.InitialDirContext; +import java.util.HashMap; import java.util.Hashtable; import java.util.List; +import java.util.Map; import static java.util.Collections.emptyList; public class Dns { - private static Result nextFakeResult = null; + private final static Map fakeResults = new HashMap<>(); - public static void fakeNextResult(final Result fakeResult) { - nextFakeResult = fakeResult; + public static void fakeResultForDomain(final String domainName, final Result fakeResult) { + fakeResults.put(domainName, fakeResult); + } + + static void resetFakeResults() { + fakeResults.clear(); } public enum Status { @@ -56,12 +62,8 @@ public class Dns { } public Result fetchRecordsOfType(final String recordType) { - if (nextFakeResult != null) { - try { - return nextFakeResult; - } finally { - nextFakeResult = null; - } + if (fakeResults.containsKey(domainName)) { + return fakeResults.get(domainName); } try { 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 afd4ca85..ce794398 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 @@ -60,12 +60,12 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { break; case Dns.Status.INVALID_NAME: - violations.add("Invalid domain name " + assetEntity.getIdentifier()); + violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'"); break; case Dns.Status.SERVICE_UNAVAILABLE: case Dns.Status.UNKNOWN_FAILURE: - violations.add("DNS request for " + assetEntity.getIdentifier() + " failed: " + result.exception()); + violations.add("[DNS] lookup failed for domain name '" + assetEntity.getIdentifier() + "': " + result.exception()); break; } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 1eaf5bbf..c1536432 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -250,7 +250,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddTopLevelAsset() { context.define("superuser-alex@hostsharing.net"); - Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); + Dns.fakeResultForDomain("example.com", new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var givenProject = realProjectRepo.findByCaption("D-1000111 default project").stream() .findAny().orElseThrow(); final var bookingItem = givenSomeTemporaryBookingItem(() -> diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index 867fd4f7..9038a052 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -4,6 +4,7 @@ import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRbacEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -39,6 +40,11 @@ class HsDomainSetupHostingAssetValidatorUnitTest { return validEntityBuilder("example.org"); } + @AfterEach + void cleanup() { + Dns.resetFakeResults(); + } + enum InvalidDomainNameIdentifier { EMPTY(""), TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"), @@ -57,7 +63,6 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @EnumSource(InvalidDomainNameIdentifier.class) void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) { // given - Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build(); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); @@ -65,12 +70,11 @@ class HsDomainSetupHostingAssetValidatorUnitTest { final var result = validator.validateEntity(givenEntity); // then - assertThat(result).containsExactly( + assertThat(result).contains( "'identifier' expected to match 'example.org', but is '"+testCase.domainName+"'" ); } - enum ValidDomainNameIdentifier { SIMPLE("example.org"), MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.de"), @@ -88,7 +92,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @EnumSource(ValidDomainNameIdentifier.class) void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) { // given - Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); + 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 validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); @@ -111,7 +115,6 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @Test void validatesReferencedEntities() { // given - Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var domainSetupHostingAssetEntity = validEntityBuilder() .parentAsset(HsHostingAssetRealEntity.builder().type(CLOUD_SERVER).build()) .assignedToAsset(HsHostingAssetRealEntity.builder().type(MANAGED_SERVER).build()) @@ -169,8 +172,10 @@ class HsDomainSetupHostingAssetValidatorUnitTest { void expectsEitherParentAssetOrBookingItem() { // given - Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + Dns.fakeResultForDomain( + domainSetupHostingAssetEntity.getIdentifier(), + new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); // when @@ -183,16 +188,16 @@ class HsDomainSetupHostingAssetValidatorUnitTest { enum DnsLookupFailureTestCase { SERVICE_UNAVAILABLE( new ServiceUnavailableException("no Internet connection"), - "DNS request for example.org failed: javax.naming.ServiceUnavailableException: no Internet connection"), + "[DNS] lookup failed for domain name 'example.org': javax.naming.ServiceUnavailableException: no Internet connection"), NAME_NOT_FOUND( - new NameNotFoundException("domain not registered"), + new NameNotFoundException("domain name not found"), null), // no INVALID_NAME( new InvalidNameException("domain name too long or whatever"), - "Invalid domain name example.org"), + "[DNS] invalid domain name 'example.org'"), UNKNOWN_FAILURE( new NamingException("some other problem"), - "DNS request for example.org failed: javax.naming.NamingException: some other problem"); + "[DNS] lookup failed for domain name 'example.org': javax.naming.NamingException: some other problem"); public final NamingException givenException; public final String expectedErrorMessage; @@ -208,8 +213,10 @@ class HsDomainSetupHostingAssetValidatorUnitTest { void handlesDnsLookupFailures(final DnsLookupFailureTestCase testCase) { // given - Dns.fakeNextResult(Dns.Result.fromException(testCase.givenException)); final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + Dns.fakeResultForDomain( + domainSetupHostingAssetEntity.getIdentifier(), + Dns.Result.fromException(testCase.givenException)); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); // when @@ -223,6 +230,24 @@ class HsDomainSetupHostingAssetValidatorUnitTest { } } + @Test + void allowSetupOfNonExistingSubdomainOfRegistrarLevelDomain() { + + // given + final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + final var domainName = domainSetupHostingAssetEntity.getIdentifier(); + Dns.fakeResultForDomain( + domainName, + Dns.Result.fromException(new NameNotFoundException("domain not registered"))); + 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) -- 2.39.5 From 0393e8b6974faa291f349f6b63dd4fd79b0eacf6 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 6 Sep 2024 15:16:26 +0200 Subject: [PATCH 09/16] fix salt problem for yescrypt hashes in HashGenerator --- .../hsadminng/hash/HashGenerator.java | 33 ++++++++++++++++--- .../hash/LinuxEtcShadowHashGenerator.java | 6 ++-- .../hsadminng/hash/HashGeneratorUnitTest.java | 13 ++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java index 44f41281..cd16b697 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java @@ -31,22 +31,37 @@ public final class HashGenerator { public enum Algorithm { LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), - LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"), + LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y", "j9T$") { + @Override + String enrichedSalt(final String salt) { + return prefix + "$" + (salt.startsWith(optionalParam) ? salt : optionalParam + salt); + } + }, MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"), SCRAM_SHA256(PostgreSQLScramSHA256::hash, "SCRAM-SHA-256"); final BiFunction implementation; final String prefix; + final String optionalParam; - Algorithm(BiFunction implementation, final String prefix) { + Algorithm(BiFunction implementation, final String prefix, final String optionalParam) { this.implementation = implementation; this.prefix = prefix; + this.optionalParam = optionalParam; + } + + Algorithm(BiFunction implementation, final String prefix) { + this(implementation, prefix, null); } static Algorithm byPrefix(final String prefix) { return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny() .orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'")); } + + String enrichedSalt(final String salt) { + return prefix + "$" + salt; + } } private final Algorithm algorithm; @@ -60,7 +75,7 @@ public final class HashGenerator { this.algorithm = algorithm; } - public static void enableChouldBeHash(final boolean enable) { + public static void enableCouldBeHash(final boolean enable) { couldBeHashEnabled = enable; } @@ -73,7 +88,11 @@ public final class HashGenerator { throw new IllegalStateException("no password given"); } - return algorithm.implementation.apply(this, plaintextPassword); + final var hash = algorithm.implementation.apply(this, plaintextPassword); + if (hash.length() < plaintextPassword.length()) { + throw new AssertionError("generated hash too short: " + hash); + } + return hash; } public String hashIfNotYetHashed(final String plaintextPasswordOrHash) { @@ -102,4 +121,10 @@ public final class HashGenerator { } return withSalt(stringBuilder.toString()); } + + public static void main(String[] args) { + System.out.println( + HashGenerator.using(Algorithm.LINUX_YESCRYPT).withRandomSalt().hash("my plaintext domain transfer passphrase") + ); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java index aaed6fd0..b5aa58b4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java @@ -10,7 +10,7 @@ public class LinuxEtcShadowHashGenerator { throw new IllegalStateException("no salt given"); } - return NativeCryptLibrary.INSTANCE.crypt(payload, "$" + generator.getAlgorithm().prefix + "$" + generator.getSalt()); + return NativeCryptLibrary.INSTANCE.crypt(payload, "$" + generator.getAlgorithm().enrichedSalt(generator.getSalt())); } public static void verify(final String givenHash, final String payload) { @@ -22,8 +22,8 @@ public class LinuxEtcShadowHashGenerator { final var algorithm = HashGenerator.Algorithm.byPrefix(parts[1]); final var salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3]; - final var calcualatedHash = HashGenerator.using(algorithm).withSalt(salt).hash(payload); - if (!calcualatedHash.equals(givenHash)) { + final var calculatedHash = HashGenerator.using(algorithm).withSalt(salt).hash(payload); + if (!calculatedHash.equals(givenHash)) { throw new IllegalArgumentException("invalid password"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java index aa5b6369..fcb2ce3d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java @@ -6,6 +6,7 @@ import java.nio.charset.Charset; import java.util.Base64; import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_SHA512; +import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_YESCRYPT; import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.MYSQL_NATIVE; import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.SCRAM_SHA256; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +58,18 @@ class HashGeneratorUnitTest { assertThat(throwable).hasMessage("invalid password"); } + @Test + void generatesLinuxSha512PasswordHash() { + final var hash = HashGenerator.using(LINUX_SHA512).withSalt("ooei1HK6JXVaI7KC").hash(GIVEN_PASSWORD); + assertThat(hash).isEqualTo(GIVEN_LINUX_GENERATED_SHA512_HASH); + } + + @Test + void generatesLinuxYescriptPasswordHash() { + final var hash = HashGenerator.using(LINUX_YESCRYPT).withSalt("wgYACPmBXvlMg2MzeZA0p1").hash(GIVEN_PASSWORD); + assertThat(hash).isEqualTo(GIVEN_LINUX_GENERATED_YESCRYPT_HASH); + } + @Test void generatesMySqlNativePasswordHash() { final var hash = HashGenerator.using(MYSQL_NATIVE).hash("Test1234"); -- 2.39.5 From c74c0be206d67cb297d73bc6df830583a5895978 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 6 Sep 2024 16:51:45 +0200 Subject: [PATCH 10/16] 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) -- 2.39.5 From 5b6cf99b9cd9d7b9ade2f181e494d712fd062895 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 9 Sep 2024 10:55:28 +0200 Subject: [PATCH 11/16] DNS TXT Record Verification using a random string --- .../HsDomainSetupBookingItemValidator.java | 33 ++++++++++++++----- .../HsDomainSetupHostingAssetValidator.java | 6 ++-- ...mainSetupBookingItemValidatorUnitTest.java | 2 +- ...ainSetupHostingAssetValidatorUnitTest.java | 10 +++--- 4 files changed, 34 insertions(+), 17 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 d540ca16..664e7485 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 @@ -1,13 +1,14 @@ 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.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 { @@ -27,7 +28,7 @@ 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"; + public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode"; HsDomainSetupBookingItemValidator() { super( @@ -35,8 +36,9 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { .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(), - passwordProperty(VERIFICATION_PASSPHRASE_PROPERTY_NAME).minLength(8).maxLength(64) - .hashedUsing(LINUX_YESCRYPT).writeOnly().optional() + stringProperty(VERIFICATION_CODE_PROPERTY_NAME) + .readOnly().initializedBy(HsDomainSetupBookingItemValidator::generateVerificationCode) + ); } @@ -44,11 +46,26 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { public List validateEntity(final HsBookingItem bookingItem) { final var violations = new ArrayList(); final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); - if ( !bookingItem.isLoaded() && - domainName.matches("hostsharing.(com|net|org|coop)") ) { - violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name"); + if (!bookingItem.isLoaded() && + domainName.matches("hostsharing.(com|net|org|coop)")) { + violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + + "' is a forbidden Hostsharing domain name"); } violations.addAll(super.validateEntity(bookingItem)); return violations; } + + private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) { + final var alphaNumeric = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + final var secureRandom = new SecureRandom(); + final var sb = new StringBuilder(); + for (int i = 0; i < 40; ++i) { + if ( i > 0 && i % 4 == 0 ) { + sb.append("-"); + } + sb.append(alphaNumeric.charAt(secureRandom.nextInt(alphaNumeric.length()))); + } + return sb.toString(); + } + } 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 2eb52144..8d5cf2e4 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,10 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { final var result = new Dns(assetEntity.getIdentifier()).fetchRecordsOfType("TXT"); switch ( result.status() ) { case Dns.Status.SUCCESS: - 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(); + 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(); if (found.isEmpty()) { - violations.add("[DNS] no TXT record 'Hostsharing-domain-setup-verification=...' with valid hash found for domain name '" + assetEntity.getIdentifier() + "'"); + violations.add("[DNS] no TXT record 'Hostsharing-domain-setup-verification="+code+"' with valid hash found for domain name '" + assetEntity.getIdentifier() + "'"); } break; 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 4b48f32a..1c7758cc 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 @@ -130,6 +130,6 @@ class HsDomainSetupBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?(ofEntries( - entry("domainName", domainName), - entry("verificationPassphrase", "some secret verification passphrase") + entry("domainName", domainName) ))) .build(); HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem); @@ -132,7 +131,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { final var result = validator.validateEntity(domainSetupHostingAssetEntity); // then - assertThat(result).containsExactlyInAnyOrder( + assertThat(result).contains( "'DOMAIN_SETUP:example.org.bookingItem' or parentItem must be null but is of type CLOUD_SERVER", "'DOMAIN_SETUP:example.org.parentAsset' must be null or of type DOMAIN_SETUP but is of type CLOUD_SERVER", "'DOMAIN_SETUP:example.org.assignedToAsset' must be null but is of type MANAGED_SERVER"); @@ -260,6 +259,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { // given final var domainSetupHostingAssetEntity = validEntityBuilder().build(); final var domainName = domainSetupHostingAssetEntity.getIdentifier(); + final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationCode", String.class); Dns.fakeResultForDomain( domainName, Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH")); @@ -269,7 +269,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { 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'"); + assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification=" + expectedHash + "' with valid hash found for domain name 'example.org'"); } @Test @@ -278,7 +278,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { // given final var domainSetupHostingAssetEntity = validEntityBuilder().build(); final var domainName = domainSetupHostingAssetEntity.getIdentifier(); - final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationPassphrase", String.class); + final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationCode", String.class); Dns.fakeResultForDomain( domainName, Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash)); -- 2.39.5 From 888e53397d6769bc1be60f8d6919171993dd2f79 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 9 Sep 2024 15:01:47 +0200 Subject: [PATCH 12/16] DNS TXT Record Verification against super-domain --- .../hs/hosting/asset/validators/Dns.java | 17 +++- .../HsDomainSetupHostingAssetValidator.java | 82 +++++++++++------- ...ainSetupHostingAssetValidatorUnitTest.java | 84 +++++++++++++++++-- 3 files changed, 139 insertions(+), 44 deletions(-) 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 b99a0fc9..ef3a3525 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 @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.Optional; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; @@ -39,6 +40,14 @@ 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 ? emptyList() @@ -52,10 +61,10 @@ public class Dns { public static Result fromException(final NamingException exception) { return switch (exception) { - case ServiceUnavailableException exc -> new Result(Status.SERVICE_UNAVAILABLE, null, exc); - case NameNotFoundException exc -> new Result(Status.NAME_NOT_FOUND, null, exc); - case InvalidNameException exc -> new Result(Status.INVALID_NAME, null, exc); - case NamingException exc -> new Result(Status.UNKNOWN_FAILURE, null, exc); + case ServiceUnavailableException exc -> new Result(Status.SERVICE_UNAVAILABLE, emptyList(), exc); + case NameNotFoundException exc -> new Result(Status.NAME_NOT_FOUND, emptyList(), exc); + case InvalidNameException exc -> new Result(Status.INVALID_NAME, emptyList(), exc); + case NamingException exc -> new Result(Status.UNKNOWN_FAILURE, emptyList(), exc); }; } } 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 8d5cf2e4..8a54f566 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 @@ -4,9 +4,11 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import java.util.ArrayList; import java.util.List; +import java.util.Optional; 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.HsDomainHttpSetupHostingAssetValidator.SUBDOMAIN_NAME_REGEX; class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { @@ -15,45 +17,55 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName"; HsDomainSetupHostingAssetValidator() { - super( DOMAIN_SETUP, + super( + DOMAIN_SETUP, AlarmContact.isOptional(), 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) { + // // 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 result = new Dns(assetEntity.getIdentifier()).fetchRecordsOfType("TXT"); - switch ( result.status() ) { + final var domainName = assetEntity.getIdentifier(); + final var dnsResult = new Dns(domainName).fetchRecordsOfType("TXT"); + switch (dnsResult.status()) { case Dns.Status.SUCCESS: 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()) { - 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; @@ -65,22 +77,28 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'"); break; - case Dns.Status.SERVICE_UNAVAILABLE: + case Dns.Status.SERVICE_UNAVAILABLE: 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; } - violations.addAll(super.validateEntity(assetEntity)); return violations; } + private static Optional findTxtRecord(final Dns.Result result, final String expectedTxtRecordValue) { + return result.records().stream() + .filter(r -> r.contains(expectedTxtRecordValue)) + .findAny(); + } + @Override protected Pattern identifierPattern(final HsHostingAsset assetEntity) { - if ( assetEntity.getBookingItem() != null ) { - final var bookingItemDomainName = assetEntity.getBookingItem().getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); - return Pattern.compile(bookingItemDomainName, Pattern.CASE_INSENSITIVE|Pattern.LITERAL); + if (assetEntity.getBookingItem() != null) { + final var bookingItemDomainName = assetEntity.getBookingItem() + .getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); + return Pattern.compile(bookingItemDomainName, Pattern.CASE_INSENSITIVE | Pattern.LITERAL); } final var parentDomainName = assetEntity.getParentAsset().getIdentifier(); return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index 3ba87810..2829706a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -76,7 +76,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { // then 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 - @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) { // given final var domainSetupHostingAssetEntity = validEntityBuilder() @@ -236,7 +236,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { } @Test - void allowSetupOfNonExistingSubdomainOfRegistrarLevelDomain() { + void allowSetupOfAvailableRegistrableDomain() { // given final var domainSetupHostingAssetEntity = validEntityBuilder().build(); @@ -254,12 +254,13 @@ class HsDomainSetupHostingAssetValidatorUnitTest { } @Test - void rejectSetupOfExistingDomainWithInvalidDnsVerification() { + void rejectSetupOfExistingRegistrableDomainWithoutValidDnsVerification() { // given final var domainSetupHostingAssetEntity = validEntityBuilder().build(); 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( domainName, Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH")); @@ -269,16 +270,18 @@ class HsDomainSetupHostingAssetValidatorUnitTest { final var result = validator.validateEntity(domainSetupHostingAssetEntity); // 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 - void allowSetupOfExistingDomainWithValidDnsVerification() { + void allowSetupOfExistingRegistrableDomainWithValidDnsVerification() { // given final var domainSetupHostingAssetEntity = validEntityBuilder().build(); 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( domainName, Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash)); @@ -291,6 +294,71 @@ class HsDomainSetupHostingAssetValidatorUnitTest { 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) { final var bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) -- 2.39.5 From 17a5aa2ff4782c7d64f9daafa1d4525c06d923b2 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 9 Sep 2024 17:09:45 +0200 Subject: [PATCH 13/16] no verification necessary directly for direct subdomains of registrar-level-domains if subdomain does not yet exist --- .../HsDomainSetupBookingItemValidator.java | 15 +---- .../hs/hosting/asset/validators/Dns.java | 39 +++++++++--- .../HsDomainSetupHostingAssetValidator.java | 59 +++++++++---------- ...mainSetupBookingItemValidatorUnitTest.java | 30 ++-------- .../hosting/asset/validators/DnsUnitTest.java | 17 ++++++ ...ainSetupHostingAssetValidatorUnitTest.java | 33 ++++++++++- 6 files changed, 113 insertions(+), 80 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/DnsUnitTest.java 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}(? Date: Tue, 10 Sep 2024 09:51:34 +0200 Subject: [PATCH 14/16] improve business tests by creating a DSL --- .../HsDomainSetupHostingAssetValidator.java | 2 +- ...ainSetupHostingAssetValidatorUnitTest.java | 282 +++++++++--------- 2 files changed, 139 insertions(+), 145 deletions(-) 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 08ac5179..392eba16 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 @@ -44,7 +44,7 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { if (verificationFound.isEmpty()) { violations.add( "[DNS] no TXT record '" + expectedTxtRecordValue + - "' found for domain name '" + domainName + "'"); + "' found for domain name '" + domainName + "' (nor in its super-domain)"); } break; } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index d275e6f6..2baa7e04 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -16,7 +16,9 @@ import javax.naming.NameNotFoundException; import javax.naming.NamingException; import javax.naming.ServiceUnavailableException; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.function.Function; import static java.util.Map.entry; import static java.util.Map.ofEntries; @@ -27,13 +29,18 @@ import static org.assertj.core.api.Assertions.assertThat; class HsDomainSetupHostingAssetValidatorUnitTest { - static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder(final String domainName) { - final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder() - .type(HsBookingItemType.DOMAIN_SETUP) - .resources(new HashMap<>(ofEntries( - entry("domainName", domainName) - ))) - .build(); + public static final Dns.Result DOMAIN_NOT_REGISTERED = Dns.Result.fromException(new NameNotFoundException( + "domain not registered")); + + static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder( + final String domainName, + final Function, HsBookingItemRealEntity> buildBookingItem) { + final HsBookingItemRealEntity bookingItem = buildBookingItem.apply( + HsBookingItemRealEntity.builder() + .type(HsBookingItemType.DOMAIN_SETUP) + .resources(new HashMap<>(ofEntries( + entry("domainName", domainName) + )))); HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem); return HsHostingAssetRbacEntity.builder() .type(DOMAIN_SETUP) @@ -41,8 +48,8 @@ class HsDomainSetupHostingAssetValidatorUnitTest { .identifier(domainName); } - static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder() { - return validEntityBuilder("example.org"); + static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder validEntityBuilder(final String domainName) { + return validEntityBuilder(domainName, HsBookingItemRealEntity.HsBookingItemRealEntityBuilder::build); } @AfterEach @@ -50,6 +57,8 @@ class HsDomainSetupHostingAssetValidatorUnitTest { Dns.resetFakeResults(); } + //===================================================================================================================== + enum InvalidDomainNameIdentifier { EMPTY(""), TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"), @@ -68,7 +77,12 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @EnumSource(InvalidDomainNameIdentifier.class) void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) { // given - final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build(); + final var givenEntity = validEntityBuilder(testCase.domainName, + bib -> bib.resources(new HashMap<>(ofEntries( + entry("domainName", "example.org") + ))).build() + ).build(); + fakeValidDnsVerification(givenEntity); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when @@ -98,11 +112,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) { // given 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)); + fakeValidDnsVerification(givenEntity); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when @@ -112,6 +122,13 @@ class HsDomainSetupHostingAssetValidatorUnitTest { assertThat(result).isEmpty(); } + private static void fakeValidDnsVerification(final HsHostingAssetRbacEntity givenEntity) { + final var expectedHash = givenEntity.getBookingItem().getDirectValue("verificationCode", String.class); + Dns.fakeResultForDomain( + givenEntity.getIdentifier(), + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash)); + } + @Test void containsNoProperties() { // when @@ -124,10 +141,10 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @Test void validatesReferencedEntities() { // given - final var domainSetupHostingAssetEntity = validEntityBuilder() + final var domainSetupHostingAssetEntity = validEntityBuilder("example.org", + bib -> bib.type(HsBookingItemType.CLOUD_SERVER).build()) .parentAsset(HsHostingAssetRealEntity.builder().type(CLOUD_SERVER).build()) .assignedToAsset(HsHostingAssetRealEntity.builder().type(MANAGED_SERVER).build()) - .bookingItem(HsBookingItemRealEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) .build(); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); @@ -144,9 +161,11 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @Test void rejectsDomainNameNotMatchingBookingItemDomainName() { // given - final var domainSetupHostingAssetEntity = validEntityBuilder() - .identifier("not-matching-booking-item-domain-name.org") - .build(); + final var domainSetupHostingAssetEntity = validEntityBuilder("not-matching-booking-item-domain-name.org", + bib -> bib.resources(new HashMap<>(ofEntries( + entry("domainName", "example.org") + ))).build() + ).build(); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); // when @@ -161,10 +180,9 @@ class HsDomainSetupHostingAssetValidatorUnitTest { @ValueSource(strings = { "not-matching-booking-item-domain-name.org", "indirect.subdomain.example.org" }) void rejectsDomainNameWhichIsNotADirectSubdomainOfParentAsset(final String newDomainName) { // given - final var domainSetupHostingAssetEntity = validEntityBuilder() + final var domainSetupHostingAssetEntity = validEntityBuilder(newDomainName) .bookingItem(null) .parentAsset(createValidParentDomainSetupAsset("example.org")) - .identifier(newDomainName) .build(); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); @@ -178,10 +196,13 @@ class HsDomainSetupHostingAssetValidatorUnitTest { } @Test - void expectsEitherParentAssetOrBookingItem() { + void rejectsIfNeitherBookingItemNorParentAssetAreSet() { // given - final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + final var domainSetupHostingAssetEntity = validEntityBuilder("example.org") + .bookingItem(null) + .parentAsset(null) + .build(); Dns.fakeResultForDomain( domainSetupHostingAssetEntity.getIdentifier(), new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null)); @@ -191,7 +212,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { final var result = validator.validateEntity(domainSetupHostingAssetEntity); // then - assertThat(result).isEmpty(); + assertThat(result).containsExactly("'DOMAIN_SETUP:example.org.bookingItem' must be of type DOMAIN_SETUP but is null"); } enum DnsLookupFailureTestCase { @@ -222,7 +243,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest { void handlesDnsLookupFailures(final DnsLookupFailureTestCase testCase) { // given - final var domainSetupHostingAssetEntity = validEntityBuilder().build(); + final var domainSetupHostingAssetEntity = validEntityBuilder("example.org").build(); Dns.fakeResultForDomain( domainSetupHostingAssetEntity.getIdentifier(), Dns.Result.fromException(testCase.givenException)); @@ -239,153 +260,65 @@ class HsDomainSetupHostingAssetValidatorUnitTest { } } + //===================================================================================================================== + @Test void allowSetupOfAvailableRegistrableDomain() { - - // given - final var domainSetupHostingAssetEntity = validEntityBuilder().build(); - final var domainName = domainSetupHostingAssetEntity.getIdentifier(); - Dns.fakeResultForDomain( - domainName, - Dns.Result.fromException(new NameNotFoundException("domain not registered"))); - final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); - - // when - final var result = validator.validateEntity(domainSetupHostingAssetEntity); - - // then - assertThat(result).isEmpty(); + domainSetupFor("example.com").notRegistered() + .isAccepted(); } @Test void rejectSetupOfExistingRegistrableDomainWithoutValidDnsVerification() { - - // given - final var domainSetupHostingAssetEntity = validEntityBuilder().build(); - final var domainName = domainSetupHostingAssetEntity.getIdentifier(); - final var expectedHash = domainSetupHostingAssetEntity.getBookingItem() - .getDirectValue("verificationCode", String.class); - 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-code=" + expectedHash - + "' found for domain name 'example.org'"); + domainSetupFor("example.com").registered() + .isRejectedWithCauseMissingVerificationIn("example.com"); } @Test void allowSetupOfExistingRegistrableDomainWithValidDnsVerification() { - - // given - final var domainSetupHostingAssetEntity = validEntityBuilder().build(); - final var domainName = domainSetupHostingAssetEntity.getIdentifier(); - final var expectedHash = domainSetupHostingAssetEntity.getBookingItem() - .getDirectValue("verificationCode", 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(); + domainSetupFor("example.org").registeredWithVerification() + .isAccepted(); } @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(); + domainSetupFor("sub.example.org").notRegistered().withVerificationIn("example.org") + .isAccepted(); } @Test - void RejectSetupOfUnregisteredSubdomainWithoutDnsVerificationInSuperDomain() { + void rejectSetupOfExistingRegistrableDomainWithInvalidDnsVerification() { + domainSetupFor("example.com").registeredWithInvalidVerification() + .isRejectedWithCauseMissingVerificationIn("example.com"); + } - // 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()); + @Test + void acceptSetupOfRegisteredSubdomainWithInvalidDnsVerificationButValidDnsVerificationInSuperDomain() { + domainSetupFor("sub.example.com").registeredWithInvalidVerification().withVerificationIn("example.com") + .isAccepted(); + } - // 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 rejectSetupOfUnregisteredSubdomainWithoutDnsVerificationInSuperDomain() { + domainSetupFor("sub.example.org").notRegistered() + .isRejectedWithCauseMissingVerificationIn("example.org"); } @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(); + domainSetupFor("sub.example.org").registered() + .withVerificationIn("example.org") + .isAccepted(); } @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-code=" + expectedHash - + "' found for domain name 'sub.example.org'"); + domainSetupFor("sub.example.org").registered() + .isRejectedWithCauseMissingVerificationIn("sub.example.org"); } + //==================================================================================================================== + private static HsHostingAssetRealEntity createValidParentDomainSetupAsset(final String parentDomainName) { final var bookingItem = HsBookingItemRealEntity.builder() .type(HsBookingItemType.DOMAIN_SETUP) @@ -399,4 +332,65 @@ class HsDomainSetupHostingAssetValidatorUnitTest { .identifier(parentDomainName).build(); return parentAsset; } + + class DomainSetupBuilder { + + private final HsHostingAssetRbacEntity domainAsset; + private final String expectedHash; + + public DomainSetupBuilder(final String domainName) { + domainAsset = validEntityBuilder(domainName).build(); + expectedHash = domainAsset.getBookingItem().getDirectValue("verificationCode", String.class); + } + + DomainSetupBuilder notRegistered() { + Dns.fakeResultForDomain(domainAsset.getIdentifier(), DOMAIN_NOT_REGISTERED); + return this; + } + + DomainSetupBuilder registered() { + Dns.fakeResultForDomain( + domainAsset.getIdentifier(), + Dns.Result.fromRecords()); + return this; + } + + DomainSetupBuilder registeredWithInvalidVerification() { + Dns.fakeResultForDomain( + domainAsset.getIdentifier(), + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH")); + return this; + } + + DomainSetupBuilder registeredWithVerification() { + withVerificationIn(domainAsset.getIdentifier()); + return this; + } + + DomainSetupBuilder withVerificationIn(final String domainName) { + Dns.fakeResultForDomain( + domainName, + Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash)); + return this; + } + + void isRejectedWithCauseMissingVerificationIn(final String domainName) { + assertThat(validate()).contains( + "[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash + + "' found for domain name '" + domainName + "' (nor in its super-domain)"); + } + + void isAccepted() { + assertThat(validate()).isEmpty(); + } + + private List validate() { + final var validator = HostingAssetEntityValidatorRegistry.forType(DOMAIN_SETUP); + return validator.validateEntity(domainAsset); + } + } + + private DomainSetupBuilder domainSetupFor(final String domainName) { + return new DomainSetupBuilder(domainName); + } } -- 2.39.5 From 6c6f850f353d8661d49b19f99fb3db6b47b7fa51 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 10 Sep 2024 10:29:56 +0200 Subject: [PATCH 15/16] some more tests --- ...ainSetupHostingAssetValidatorUnitTest.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index 2baa7e04..ce5c8246 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -268,6 +268,24 @@ class HsDomainSetupHostingAssetValidatorUnitTest { .isAccepted(); } + @Test + void allowSetupOfAvailableRegistrable2ndLevelDomain() { + domainSetupFor("example.co.uk").notRegistered() + .isAccepted(); + } + + @Test + void rejectSetupOfRegisteredRegistrable2ndLevelDomainWithoutVerification() { + domainSetupFor("example.co.uk").registered() + .isRejectedWithCauseMissingVerificationIn("example.co.uk"); + } + + @Test + void allowSetupOfRegisteredRegistrable2ndLevelDomainWithVerification() { + domainSetupFor("example.co.uk").registeredWithVerification() + .isAccepted(); + } + @Test void rejectSetupOfExistingRegistrableDomainWithoutValidDnsVerification() { domainSetupFor("example.com").registered() @@ -375,9 +393,11 @@ class HsDomainSetupHostingAssetValidatorUnitTest { } void isRejectedWithCauseMissingVerificationIn(final String domainName) { - assertThat(validate()).contains( + assertThat(validate()).containsAnyOf( "[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash - + "' found for domain name '" + domainName + "' (nor in its super-domain)"); + + "' found for domain name '" + domainName + "' (nor in its super-domain)", + "[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash + + "' found for domain name '" + domainName + "'"); } void isAccepted() { -- 2.39.5 From 003eb294549b005e7ee88317155d40c6f26e214a Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 10 Sep 2024 13:14:36 +0200 Subject: [PATCH 16/16] fix issues from code review --- .../HsDomainSetupBookingItemValidator.java | 3 +- .../hs/hosting/asset/validators/Dns.java | 13 +++- .../HsDomainSetupHostingAssetValidator.java | 33 +++++---- ...mainSetupBookingItemValidatorUnitTest.java | 42 ++++++++++- ...sHostingAssetControllerAcceptanceTest.java | 6 ++ .../hosting/asset/validators/DnsUnitTest.java | 22 ++++-- ...ainSetupHostingAssetValidatorUnitTest.java | 72 ++++++++++++------- 7 files changed, 142 insertions(+), 49 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 38234c07..3d62b765 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 @@ -20,6 +20,7 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { HsDomainSetupBookingItemValidator() { super( stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce() + .maxLength(253) .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(), @@ -34,7 +35,7 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { final var violations = new ArrayList(); final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class); if (!bookingItem.isLoaded() && - domainName.matches("hostsharing.(com|net|org|coop)")) { + domainName.matches("hostsharing.(com|net|org|coop|de)")) { violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name"); } 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 d3a3bb3a..037b95c0 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 @@ -49,16 +49,25 @@ public class Dns { return Optional.empty(); } - public static boolean isRegistrarLevel(final String domainName) { + public static boolean isRegistrarLevelDomain(final String domainName) { return stream(REGISTRAR_LEVEL_DOMAIN_PATTERN) .anyMatch(p -> p.matcher(domainName).matches()); } + /** + * @param domainName a fully qualified domain name + * @return true if `domainName` can be registered at a registrar, false if it's a subdomain of such or a registrar-level domain itself + */ + public static boolean isRegistrableDomain(final String domainName) { + return !isRegistrarLevelDomain(domainName) && + superDomain(domainName).map(Dns::isRegistrarLevelDomain).orElse(false); + } + public static void fakeResultForDomain(final String domainName, final Result fakeResult) { fakeResults.put(domainName, fakeResult); } - static void resetFakeResults() { + public static void resetFakeResults() { fakeResults.clear(); } 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 392eba16..40530ad1 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 @@ -2,7 +2,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; 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; @@ -27,8 +26,12 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { @Override public List validateEntity(final HsHostingAsset assetEntity) { + final var violations = // new ArrayList(); + super.validateEntity(assetEntity); + if (!violations.isEmpty()) { + return violations; + } - 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); @@ -50,10 +53,8 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { } 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) { + if (isDnsVerificationRequiredForUnregisteredDomain(assetEntity)) { + final var superDomain = superDomain(domainName); final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get(); final var verificationFoundInSuperDomain = superDomain.flatMap(superDomainName -> findTxtRecord( new Dns(superDomainName).fetchRecordsOfType("TXT"), @@ -77,17 +78,9 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { violations.add("[DNS] lookup failed for domain name '" + assetEntity.getIdentifier() + "': " + dnsResult.exception()); break; } - - violations.addAll(super.validateEntity(assetEntity)); return violations; } - private static Optional findTxtRecord(final Dns.Result result, final String expectedTxtRecordValue) { - return result.records().stream() - .filter(r -> r.contains(expectedTxtRecordValue)) - .findAny(); - } - @Override protected Pattern identifierPattern(final HsHostingAsset assetEntity) { if (assetEntity.getBookingItem() != null) { @@ -98,4 +91,16 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { final var parentDomainName = assetEntity.getParentAsset().getIdentifier(); return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE); } + + private static boolean isDnsVerificationRequiredForUnregisteredDomain(final HsHostingAsset assetEntity) { + return !Dns.isRegistrableDomain(assetEntity.getIdentifier()) + && assetEntity.getParentAsset() == null; + } + + + private static Optional findTxtRecord(final Dns.Result result, final String expectedTxtRecordValue) { + return result.records().stream() + .filter(r -> r.contains(expectedTxtRecordValue)) + .findAny(); + } } 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 352c71de..9fbdac45 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 @@ -12,10 +12,12 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP; +import static org.apache.commons.lang3.StringUtils.right; import static org.assertj.core.api.Assertions.assertThat; class HsDomainSetupBookingItemValidatorUnitTest { + public static final String TOO_LONG_DOMAIN_NAME = "asdfghijklmnopqrstuvwxyz0123456789.".repeat(8) + "example.org"; final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() .debitorNumber(12345) .build(); @@ -44,6 +46,42 @@ class HsDomainSetupBookingItemValidatorUnitTest { assertThat(result).isEmpty(); } + @Test + void acceptsMaximumDomainNameLength() { + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)) + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsTooLongTotalName() { + final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder() + .type(DOMAIN_SETUP) + .project(project) + .caption("Test-Domain") + .resources(Map.ofEntries( + entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)) + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity); + + // then + assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.domainName' length is expected to be at max 253 but length of 'dfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.example.org' is 254"); + } + @ParameterizedTest @ValueSource(strings = { "de", "com", "net", "org", "actually-any-top-level-domain", @@ -81,7 +119,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { @ParameterizedTest @ValueSource(strings = { - "hostsharing.net", "hostsharing.org", "hostsharing.com", "hostsharing.coop" + "hostsharing.net", "hostsharing.org", "hostsharing.com", "hostsharing.coop", "hostsharing.de" }) void rejectHostsharingDomain(final String secondLevelRegistrarDomain) { // given @@ -111,7 +149,7 @@ class HsDomainSetupBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(? bib.resources(new HashMap<>(ofEntries( - entry("domainName", "example.org") - ))).build() - ).build(); - fakeValidDnsVerification(givenEntity); + final var givenEntity = validEntityBuilder(testCase.domainName) + .bookingItem(null) + .parentAsset(HsHostingAssetRealEntity.builder().type(DOMAIN_SETUP).identifier("example.org").build()) + .build(); + // fakeValidDnsVerification(givenEntity); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when @@ -90,26 +90,26 @@ class HsDomainSetupHostingAssetValidatorUnitTest { // then assertThat(result).contains( - "'identifier' expected to match 'example.org', but is '" + testCase.domainName + "'" + "'identifier' expected to match '(\\*|(?!-)[A-Za-z0-9-]{1,63}(?