check-domain-setup-permission #97
@ -6,6 +6,8 @@ import net.hostsharing.hsadminng.mapper.Array;
|
|||||||
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 {
|
||||||
@ -25,13 +27,16 @@ 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";
|
||||||
|
|
||||||
HsDomainSetupBookingItemValidator() {
|
HsDomainSetupBookingItemValidator() {
|
||||||
super(
|
super(
|
||||||
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
||||||
.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)
|
||||||
|
.hashedUsing(LINUX_YESCRYPT).writeOnly().optional()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import java.util.Hashtable;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
|
|
||||||
public class Dns {
|
public class Dns {
|
||||||
@ -38,13 +39,17 @@ public class Dns {
|
|||||||
|
|
||||||
public record Result(Status status, List<String> records, NamingException exception) {
|
public record Result(Status status, List<String> records, NamingException exception) {
|
||||||
|
|
||||||
public static Result fromRecords(final NamingEnumeration<?> enumeration) {
|
public static Result fromRecords(final NamingEnumeration<?> recordEnumeration) {
|
||||||
final List<String> records = enumeration == null
|
final List<String> records = recordEnumeration == null
|
||||||
? emptyList()
|
? emptyList()
|
||||||
: EnumerationUtils.toList(enumeration).stream().map(Object::toString).toList();
|
: EnumerationUtils.toList(recordEnumeration).stream().map(Object::toString).toList();
|
||||||
return new Result(Status.SUCCESS, records, null);
|
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) {
|
public static Result fromException(final NamingException exception) {
|
||||||
return switch (exception) {
|
return switch (exception) {
|
||||||
case ServiceUnavailableException exc -> new Result(Status.SERVICE_UNAVAILABLE, null, exc);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -50,10 +50,12 @@ 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 found = result.records().stream().filter(r -> r.contains("TXT Hostsharing-domain-setup-verification=FIXME")).findAny();
|
final var hash = assetEntity.getBookingItem().getDirectValue("verificationPassphrase", String.class);
|
||||||
if (found.isPresent()) {
|
final var found = result.records().stream().filter(r -> r.contains("Hostsharing-domain-setup-verification-code=" + hash)).findAny();
|
||||||
break;
|
if (found.isEmpty()) {
|
||||||
hsh-michaelhoennig marked this conversation as resolved
|
|||||||
|
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:
|
case Dns.Status.NAME_NOT_FOUND:
|
||||||
// no DNS verification necessary / FIXME: at least if the superdomain is at registrar level
|
// no DNS verification necessary / FIXME: at least if the superdomain is at registrar level
|
||||||
|
@ -129,6 +129,7 @@ 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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
|||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRbacEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRbacEntity;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
@ -14,8 +15,11 @@ import javax.naming.InvalidNameException;
|
|||||||
import javax.naming.NameNotFoundException;
|
import javax.naming.NameNotFoundException;
|
||||||
import javax.naming.NamingException;
|
import javax.naming.NamingException;
|
||||||
import javax.naming.ServiceUnavailableException;
|
import javax.naming.ServiceUnavailableException;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Map.entry;
|
||||||
|
import static java.util.Map.ofEntries;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
||||||
@ -26,10 +30,12 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
|||||||
static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder<?, ?> validEntityBuilder(final String domainName) {
|
static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder<?, ?> validEntityBuilder(final String domainName) {
|
||||||
final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder()
|
final HsBookingItemRealEntity bookingItem = HsBookingItemRealEntity.builder()
|
||||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||||
.resources(Map.ofEntries(
|
.resources(new HashMap<>(ofEntries(
|
||||||
Map.entry("domainName", domainName)
|
entry("domainName", domainName),
|
||||||
))
|
entry("verificationPassphrase", "some secret verification passphrase")
|
||||||
|
)))
|
||||||
.build();
|
.build();
|
||||||
|
HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem);
|
||||||
return HsHostingAssetRbacEntity.builder()
|
return HsHostingAssetRbacEntity.builder()
|
||||||
.type(DOMAIN_SETUP)
|
.type(DOMAIN_SETUP)
|
||||||
.bookingItem(bookingItem)
|
.bookingItem(bookingItem)
|
||||||
@ -248,11 +254,48 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
|||||||
assertThat(result).isEmpty();
|
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) {
|
private static HsHostingAssetRealEntity createValidParentDomainSetupAsset(final String parentDomainName) {
|
||||||
final var bookingItem = HsBookingItemRealEntity.builder()
|
final var bookingItem = HsBookingItemRealEntity.builder()
|
||||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||||
.resources(Map.ofEntries(
|
.resources(ofEntries(
|
||||||
Map.entry("domainName", parentDomainName)
|
entry("domainName", parentDomainName)
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
final var parentAsset = HsHostingAssetRealEntity.builder()
|
final var parentAsset = HsHostingAssetRealEntity.builder()
|
||||||
|
@ -1352,7 +1352,7 @@ public class ImportHostingAssets extends BaseOfficeDataImport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void importDatabaseUsers(final String[] header, final List<String[]> records) {
|
private void importDatabaseUsers(final String[] header, final List<String[]> records) {
|
||||||
HashGenerator.enableChouldBeHash(true);
|
HashGenerator.enableCouldBeHash(true);
|
||||||
final var columns = new Columns(header);
|
final var columns = new Columns(header);
|
||||||
records.stream()
|
records.stream()
|
||||||
.map(this::trimAll)
|
.map(this::trimAll)
|
||||||
|
Loading…
Reference in New Issue
Block a user
use getParentAsset == null + Test