add DNS TXT record verification (WIP)

This commit is contained in:
Michael Hoennig 2024-09-05 16:36:55 +02:00
parent 4a3af3f6fe
commit e94f2f254a
5 changed files with 118 additions and 2 deletions

View File

@ -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<String> 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);
}
}
}

View File

@ -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<String> validateEntity(final HsHostingAsset assetEntity) {
final var violations = new ArrayList<String>();
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 ) {

View File

@ -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(() ->

View File

@ -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());

View File

@ -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
);