Compare commits
4 Commits
e94f2f254a
...
c74c0be206
Author | SHA1 | Date | |
---|---|---|---|
|
c74c0be206 | ||
|
0393e8b697 | ||
|
8316a88bce | ||
|
bab85c5581 |
@ -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")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(() ->
|
||||||
|
@ -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()
|
||||||
|
@ -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