Compare commits

...

4 Commits

10 changed files with 230 additions and 55 deletions

View File

@ -31,22 +31,37 @@ public final class HashGenerator {
public enum Algorithm { public enum Algorithm {
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), 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, "*"), MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"),
SCRAM_SHA256(PostgreSQLScramSHA256::hash, "SCRAM-SHA-256"); SCRAM_SHA256(PostgreSQLScramSHA256::hash, "SCRAM-SHA-256");
final BiFunction<HashGenerator, String, String> implementation; final BiFunction<HashGenerator, String, String> implementation;
final String prefix; final String prefix;
final String optionalParam;
Algorithm(BiFunction<HashGenerator, String, String> implementation, final String prefix) { Algorithm(BiFunction<HashGenerator, String, String> implementation, final String prefix, final String optionalParam) {
this.implementation = implementation; this.implementation = implementation;
this.prefix = prefix; this.prefix = prefix;
this.optionalParam = optionalParam;
}
Algorithm(BiFunction<HashGenerator, String, String> implementation, final String prefix) {
this(implementation, prefix, null);
} }
static Algorithm byPrefix(final String prefix) { static Algorithm byPrefix(final String prefix) {
return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny() return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny()
.orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'")); .orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'"));
} }
String enrichedSalt(final String salt) {
return prefix + "$" + salt;
}
} }
private final Algorithm algorithm; private final Algorithm algorithm;
@ -60,7 +75,7 @@ public final class HashGenerator {
this.algorithm = algorithm; this.algorithm = algorithm;
} }
public static void enableChouldBeHash(final boolean enable) { public static void enableCouldBeHash(final boolean enable) {
couldBeHashEnabled = enable; couldBeHashEnabled = enable;
} }
@ -73,7 +88,11 @@ public final class HashGenerator {
throw new IllegalStateException("no password given"); 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) { public String hashIfNotYetHashed(final String plaintextPasswordOrHash) {
@ -102,4 +121,10 @@ public final class HashGenerator {
} }
return withSalt(stringBuilder.toString()); 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")
);
}
} }

View File

@ -10,7 +10,7 @@ public class LinuxEtcShadowHashGenerator {
throw new IllegalStateException("no salt given"); 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) { 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 algorithm = HashGenerator.Algorithm.byPrefix(parts[1]);
final var salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3]; final var salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3];
final var calcualatedHash = HashGenerator.using(algorithm).withSalt(salt).hash(payload); final var calculatedHash = HashGenerator.using(algorithm).withSalt(salt).hash(payload);
if (!calcualatedHash.equals(givenHash)) { if (!calculatedHash.equals(givenHash)) {
throw new IllegalArgumentException("invalid password"); throw new IllegalArgumentException("invalid password");
} }
} }

View File

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

View File

@ -4,26 +4,33 @@ import org.apache.commons.collections4.EnumerationUtils;
import javax.naming.InvalidNameException; import javax.naming.InvalidNameException;
import javax.naming.NameNotFoundException; import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.naming.ServiceUnavailableException; import javax.naming.ServiceUnavailableException;
import javax.naming.directory.Attribute; import javax.naming.directory.Attribute;
import javax.naming.directory.InitialDirContext; import javax.naming.directory.InitialDirContext;
import java.util.HashMap;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
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 {
private static Result nextFakeResult = null; private final static Map<String, Result> fakeResults = new HashMap<>();
public static void fakeNextResult(final Result fakeResult) { public static void fakeResultForDomain(final String domainName, final Result fakeResult) {
nextFakeResult = fakeResult; fakeResults.put(domainName, fakeResult);
}
static void resetFakeResults() {
fakeResults.clear();
} }
public enum Status { public enum Status {
SUCCESS, SUCCESS,
RECORD_TYPE_NOT_FOUND,
NAME_NOT_FOUND, NAME_NOT_FOUND,
INVALID_NAME, INVALID_NAME,
SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE,
@ -31,6 +38,26 @@ 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<?> recordEnumeration) {
final List<String> records = recordEnumeration == null
? emptyList()
: 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);
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; private final String domainName;
@ -40,35 +67,25 @@ public class Dns {
} }
public Result fetchRecordsOfType(final String recordType) { public Result fetchRecordsOfType(final String recordType) {
if (nextFakeResult != null) { if (fakeResults.containsKey(domainName)) {
try { return fakeResults.get(domainName);
return nextFakeResult;
} finally {
nextFakeResult = null;
}
} }
try { try {
final var env = new Hashtable<>(); final var env = new Hashtable<>();
env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); 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 }) .getAttributes(domainName, new String[] { recordType })
.get(recordType); .get(recordType);
return new Result( return Result.fromRecords(records != null ? records.getAll() : null);
r == null ? Status.RECORD_TYPE_NOT_FOUND : Status.SUCCESS, } catch (final NamingException exception) {
r == null return Result.fromException(exception);
? 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);
} }
} }
public static void main(String[] args) {
final var result = new Dns("example.org").fetchRecordsOfType("TXT");
System.out.println(result);
}
} }

View File

@ -50,25 +50,24 @@ 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()) {
violations.add("[DNS] no TXT record 'Hostsharing-domain-setup-verification=...' with valid hash found for domain name '" + assetEntity.getIdentifier() + "'");
} }
case Dns.Status.RECORD_TYPE_NOT_FOUND:
violations.add("Domain " + assetEntity.getIdentifier() + " exists, but no record 'TXT Hostsharing-domain-setup-challenge:FIXME' found ");
break; break;
case Dns.Status.NAME_NOT_FOUND: 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; break;
case Dns.Status.INVALID_NAME: case Dns.Status.INVALID_NAME:
violations.add("Invalid domain name " + assetEntity.getIdentifier()); violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'");
break; break;
case Dns.Status.SERVICE_UNAVAILABLE: case Dns.Status.SERVICE_UNAVAILABLE:
case Dns.Status.UNKNOWN_FAILURE: 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; break;
} }

View File

@ -6,6 +6,7 @@ import java.nio.charset.Charset;
import java.util.Base64; import java.util.Base64;
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_SHA512; 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.MYSQL_NATIVE;
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.SCRAM_SHA256; import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.SCRAM_SHA256;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -57,6 +58,18 @@ class HashGeneratorUnitTest {
assertThat(throwable).hasMessage("invalid password"); 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 @Test
void generatesMySqlNativePasswordHash() { void generatesMySqlNativePasswordHash() {
final var hash = HashGenerator.using(MYSQL_NATIVE).hash("Test1234"); final var hash = HashGenerator.using(MYSQL_NATIVE).hash("Test1234");

View File

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

View File

@ -250,7 +250,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
void globalAdmin_canAddTopLevelAsset() { void globalAdmin_canAddTopLevelAsset() {
context.define("superuser-alex@hostsharing.net"); 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() final var givenProject = realProjectRepo.findByCaption("D-1000111 default project").stream()
.findAny().orElseThrow(); .findAny().orElseThrow();
final var bookingItem = givenSomeTemporaryBookingItem(() -> final var bookingItem = givenSomeTemporaryBookingItem(() ->

View File

@ -2,15 +2,24 @@ 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.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource; 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.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;
@ -21,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)
@ -35,6 +46,11 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
return validEntityBuilder("example.org"); return validEntityBuilder("example.org");
} }
@AfterEach
void cleanup() {
Dns.resetFakeResults();
}
enum InvalidDomainNameIdentifier { enum InvalidDomainNameIdentifier {
EMPTY(""), EMPTY(""),
TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"), TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"),
@ -53,7 +69,6 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
@EnumSource(InvalidDomainNameIdentifier.class) @EnumSource(InvalidDomainNameIdentifier.class)
void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) { void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) {
// given // given
Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null));
final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build(); final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
@ -61,12 +76,11 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
final var result = validator.validateEntity(givenEntity); final var result = validator.validateEntity(givenEntity);
// then // then
assertThat(result).containsExactly( assertThat(result).contains(
"'identifier' expected to match 'example.org', but is '"+testCase.domainName+"'" "'identifier' expected to match 'example.org', but is '"+testCase.domainName+"'"
); );
} }
enum ValidDomainNameIdentifier { enum ValidDomainNameIdentifier {
SIMPLE("example.org"), SIMPLE("example.org"),
MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.de"), MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.de"),
@ -84,7 +98,7 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
@EnumSource(ValidDomainNameIdentifier.class) @EnumSource(ValidDomainNameIdentifier.class)
void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) { void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) {
// given // 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 givenEntity = validEntityBuilder(testCase.domainName).identifier(testCase.domainName).build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
@ -107,7 +121,6 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
@Test @Test
void validatesReferencedEntities() { void validatesReferencedEntities() {
// given // given
Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null));
final var domainSetupHostingAssetEntity = validEntityBuilder() final var domainSetupHostingAssetEntity = validEntityBuilder()
.parentAsset(HsHostingAssetRealEntity.builder().type(CLOUD_SERVER).build()) .parentAsset(HsHostingAssetRealEntity.builder().type(CLOUD_SERVER).build())
.assignedToAsset(HsHostingAssetRealEntity.builder().type(MANAGED_SERVER).build()) .assignedToAsset(HsHostingAssetRealEntity.builder().type(MANAGED_SERVER).build())
@ -165,8 +178,110 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
void expectsEitherParentAssetOrBookingItem() { void expectsEitherParentAssetOrBookingItem() {
// given // given
Dns.fakeNextResult(new Dns.Result(Dns.Status.NAME_NOT_FOUND, null, null));
final var domainSetupHostingAssetEntity = validEntityBuilder().build(); 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
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
// then
assertThat(result).isEmpty();
}
enum DnsLookupFailureTestCase {
SERVICE_UNAVAILABLE(
new ServiceUnavailableException("no Internet connection"),
"[DNS] lookup failed for domain name 'example.org': javax.naming.ServiceUnavailableException: no Internet connection"),
NAME_NOT_FOUND(
new NameNotFoundException("domain name not found"),
null), // no
INVALID_NAME(
new InvalidNameException("domain name too long or whatever"),
"[DNS] invalid domain name 'example.org'"),
UNKNOWN_FAILURE(
new 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;
DnsLookupFailureTestCase(final NamingException givenException, final String expectedErrorMessage) {
this.givenException = givenException;
this.expectedErrorMessage = expectedErrorMessage;
}
}
@ParameterizedTest
@EnumSource(DnsLookupFailureTestCase.class)
void handlesDnsLookupFailures(final DnsLookupFailureTestCase testCase) {
// given
final var domainSetupHostingAssetEntity = validEntityBuilder().build();
Dns.fakeResultForDomain(
domainSetupHostingAssetEntity.getIdentifier(),
Dns.Result.fromException(testCase.givenException));
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();
}
}
@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();
}
@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()); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
// when // when
@ -179,8 +294,8 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
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()

View File

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