From e94f2f254a07f9c28f0ca7ee47a72d07b3a6170b Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 5 Sep 2024 16:36:55 +0200 Subject: [PATCH] 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 );