DNS TXT Record Verification using a random string
This commit is contained in:
parent
c74c0be206
commit
5b6cf99b9c
@ -1,13 +1,14 @@
|
|||||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||||
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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;
|
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||||
|
|
||||||
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||||
@ -27,7 +28,7 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
|||||||
"(co|ne|or|go|re|pe)\\.kr"
|
"(co|ne|or|go|re|pe)\\.kr"
|
||||||
);
|
);
|
||||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
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() {
|
HsDomainSetupBookingItemValidator() {
|
||||||
super(
|
super(
|
||||||
@ -35,8 +36,9 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
|||||||
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
.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")
|
.notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
|
||||||
.required(),
|
.required(),
|
||||||
passwordProperty(VERIFICATION_PASSPHRASE_PROPERTY_NAME).minLength(8).maxLength(64)
|
stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
|
||||||
.hashedUsing(LINUX_YESCRYPT).writeOnly().optional()
|
.readOnly().initializedBy(HsDomainSetupBookingItemValidator::generateVerificationCode)
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +48,24 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
|||||||
final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
|
final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
|
||||||
if (!bookingItem.isLoaded() &&
|
if (!bookingItem.isLoaded() &&
|
||||||
domainName.matches("hostsharing.(com|net|org|coop)")) {
|
domainName.matches("hostsharing.(com|net|org|coop)")) {
|
||||||
violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName + "' is a forbidden Hostsharing domain name");
|
violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName
|
||||||
|
+ "' is a forbidden Hostsharing domain name");
|
||||||
}
|
}
|
||||||
violations.addAll(super.validateEntity(bookingItem));
|
violations.addAll(super.validateEntity(bookingItem));
|
||||||
return violations;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -50,10 +50,10 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
|||||||
final var result = new Dns(assetEntity.getIdentifier()).fetchRecordsOfType("TXT");
|
final var result = new Dns(assetEntity.getIdentifier()).fetchRecordsOfType("TXT");
|
||||||
switch ( result.status() ) {
|
switch ( result.status() ) {
|
||||||
case Dns.Status.SUCCESS:
|
case Dns.Status.SUCCESS:
|
||||||
final var hash = assetEntity.getBookingItem().getDirectValue("verificationPassphrase", String.class);
|
final var code = assetEntity.getBookingItem().getDirectValue("verificationCode", String.class);
|
||||||
final var found = result.records().stream().filter(r -> r.contains("Hostsharing-domain-setup-verification-code=" + hash)).findAny();
|
final var found = result.records().stream().filter(r -> r.contains("Hostsharing-domain-setup-verification-code=" + code)).findAny();
|
||||||
if (found.isEmpty()) {
|
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;
|
break;
|
||||||
|
|
||||||
|
@ -130,6 +130,6 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
||||||
"{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[(co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, required=true, writeOnce=true}",
|
"{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[(co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, required=true, writeOnce=true}",
|
||||||
"{type=password, propertyName=verificationPassphrase, minLength=8, maxLength=64, writeOnly=true, computed=IN_PREP, hashedUsing=LINUX_YESCRYPT, undisclosed=true}");
|
"{type=string, propertyName=verificationCode, readOnly=true, computed=IN_INIT}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
|||||||
final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder()
|
final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder()
|
||||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||||
.resources(new HashMap<>(ofEntries(
|
.resources(new HashMap<>(ofEntries(
|
||||||
entry("domainName", domainName),
|
entry("domainName", domainName)
|
||||||
entry("verificationPassphrase", "some secret verification passphrase")
|
|
||||||
)))
|
)))
|
||||||
.build();
|
.build();
|
||||||
HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem);
|
HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem);
|
||||||
@ -132,7 +131,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
|||||||
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
||||||
|
|
||||||
// then
|
// 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.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.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");
|
"'DOMAIN_SETUP:example.org.assignedToAsset' must be null but is of type MANAGED_SERVER");
|
||||||
@ -260,6 +259,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
|||||||
// given
|
// given
|
||||||
final var domainSetupHostingAssetEntity = validEntityBuilder().build();
|
final var domainSetupHostingAssetEntity = validEntityBuilder().build();
|
||||||
final var domainName = domainSetupHostingAssetEntity.getIdentifier();
|
final var domainName = domainSetupHostingAssetEntity.getIdentifier();
|
||||||
|
final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationCode", String.class);
|
||||||
Dns.fakeResultForDomain(
|
Dns.fakeResultForDomain(
|
||||||
domainName,
|
domainName,
|
||||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH"));
|
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH"));
|
||||||
@ -269,7 +269,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
|||||||
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(result).contains("[DNS] no TXT record 'Hostsharing-domain-setup-verification=...' 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
|
@Test
|
||||||
@ -278,7 +278,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
|||||||
// given
|
// given
|
||||||
final var domainSetupHostingAssetEntity = validEntityBuilder().build();
|
final var domainSetupHostingAssetEntity = validEntityBuilder().build();
|
||||||
final var domainName = domainSetupHostingAssetEntity.getIdentifier();
|
final var domainName = domainSetupHostingAssetEntity.getIdentifier();
|
||||||
final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationPassphrase", String.class);
|
final var expectedHash = domainSetupHostingAssetEntity.getBookingItem().getDirectValue("verificationCode", String.class);
|
||||||
Dns.fakeResultForDomain(
|
Dns.fakeResultForDomain(
|
||||||
domainName,
|
domainName,
|
||||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
|
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
|
||||||
|
Loading…
Reference in New Issue
Block a user