check-domain-setup-permission #97
@ -1,10 +1,59 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS;
|
||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||
|
||||
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
|
||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
||||
|
||||
HsDomainSetupBookingItemValidator() {
|
||||
super(
|
||||
// no properties yet. maybe later, the setup code goes here?
|
||||
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
||||
.maxLength(253)
|
||||
.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")
|
||||
.required(),
|
||||
stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
|
||||
.readOnly().initializedBy(HsDomainSetupBookingItemValidator::generateVerificationCode)
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> validateEntity(final HsBookingItem bookingItem) {
|
||||
final var violations = new ArrayList<String>();
|
||||
final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
|
||||
if (!bookingItem.isLoaded() &&
|
||||
domainName.matches("hostsharing.(com|net|org|coop|de)")) {
|
||||
violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName
|
||||
+ "' is a forbidden Hostsharing domain name");
|
||||
}
|
||||
violations.addAll(super.validateEntity(bookingItem));
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,134 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.mapper.Array;
|
||||
import org.apache.commons.collections4.EnumerationUtils;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
import javax.naming.NameNotFoundException;
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.ServiceUnavailableException;
|
||||
import javax.naming.directory.Attribute;
|
||||
import javax.naming.directory.InitialDirContext;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public class Dns {
|
||||
|
||||
public static final String[] REGISTRAR_LEVEL_DOMAINS = Array.of(
|
||||
"[^.]+", // top-level-domains
|
||||
"(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"
|
||||
);
|
||||
public static final Pattern[] REGISTRAR_LEVEL_DOMAIN_PATTERN = stream(REGISTRAR_LEVEL_DOMAINS)
|
||||
.map(Pattern::compile)
|
||||
.toArray(Pattern[]::new);
|
||||
|
||||
private final static Map<String, Result> fakeResults = new HashMap<>();
|
||||
|
||||
public static Optional<String> superDomain(final String domainName) {
|
||||
final var parts = domainName.split("\\.", 2);
|
||||
if (parts.length == 2) {
|
||||
return Optional.of(parts[1]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static boolean isRegistrarLevelDomain(final String domainName) {
|
||||
return stream(REGISTRAR_LEVEL_DOMAIN_PATTERN)
|
||||
.anyMatch(p -> p.matcher(domainName).matches());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param domainName a fully qualified domain name
|
||||
* @return true if `domainName` can be registered at a registrar, false if it's a subdomain of such or a registrar-level domain itself
|
||||
*/
|
||||
public static boolean isRegistrableDomain(final String domainName) {
|
||||
return !isRegistrarLevelDomain(domainName) &&
|
||||
superDomain(domainName).map(Dns::isRegistrarLevelDomain).orElse(false);
|
||||
}
|
||||
|
||||
public static void fakeResultForDomain(final String domainName, final Result fakeResult) {
|
||||
fakeResults.put(domainName, fakeResult);
|
||||
}
|
||||
|
||||
public static void resetFakeResults() {
|
||||
fakeResults.clear();
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
SUCCESS,
|
||||
NAME_NOT_FOUND,
|
||||
INVALID_NAME,
|
||||
SERVICE_UNAVAILABLE,
|
||||
UNKNOWN_FAILURE
|
||||
}
|
||||
|
||||
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, emptyList(), exc);
|
||||
case NameNotFoundException exc -> new Result(Status.NAME_NOT_FOUND, emptyList(), exc);
|
||||
case InvalidNameException exc -> new Result(Status.INVALID_NAME, emptyList(), exc);
|
||||
case NamingException exc -> new Result(Status.UNKNOWN_FAILURE, emptyList(), exc);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private final String domainName;
|
||||
|
||||
public Dns(final String domainName) {
|
||||
this.domainName = domainName;
|
||||
}
|
||||
|
||||
public Result fetchRecordsOfType(final String recordType) {
|
||||
if (fakeResults.containsKey(domainName)) {
|
||||
return fakeResults.get(domainName);
|
||||
}
|
||||
|
||||
try {
|
||||
final var env = new Hashtable<>();
|
||||
env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
|
||||
final Attribute records = new InitialDirContext(env)
|
||||
.getAttributes(domainName, new String[] { recordType })
|
||||
.get(recordType);
|
||||
return Result.fromRecords(records != null ? records.getAll() : null);
|
||||
} catch (final NamingException exception) {
|
||||
return Result.fromException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
final var result = new Dns("example.org").fetchRecordsOfType("TXT");
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
}
|
@ -3,55 +3,104 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.superDomain;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainHttpSetupHostingAssetValidator.SUBDOMAIN_NAME_REGEX;
|
||||
|
||||
class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||
|
||||
private final Pattern identifierPattern;
|
||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||
|
||||
HsDomainSetupHostingAssetValidator() {
|
||||
super( DOMAIN_SETUP,
|
||||
super(
|
||||
DOMAIN_SETUP,
|
||||
AlarmContact.isOptional(),
|
||||
|
||||
NO_EXTRA_PROPERTIES);
|
||||
this.identifierPattern = Pattern.compile(FQDN_REGEX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> validateEntity(final HsHostingAsset assetEntity) {
|
||||
// TODO.impl: for newly created entities, check the permission of setting up a domain
|
||||
//
|
||||
// reject, if the domain is any of these:
|
||||
// hostsharing.com|net|org|coop, // just to be on the safe side
|
||||
// [^.}+, // top-level-domain
|
||||
// co.uk, org.uk, gov.uk, ac.uk, sch.uk,
|
||||
// com.au, net.au, org.au, edu.au, gov.au, asn.au, id.au,
|
||||
// co.jp, ne.jp, or.jp, ac.jp, go.jp,
|
||||
// com.cn, net.cn, org.cn, gov.cn, edu.cn, ac.cn,
|
||||
// com.br, net.br, org.br, gov.br, edu.br, mil.br, art.br,
|
||||
// co.in, net.in, org.in, gen.in, firm.in, ind.in,
|
||||
// com.mx, net.mx, org.mx, gob.mx, edu.mx,
|
||||
// gov.it, edu.it,
|
||||
// co.nz, net.nz, org.nz, govt.nz, ac.nz, school.nz, geek.nz, kiwi.nz,
|
||||
// co.kr, ne.kr, or.kr, go.kr, re.kr, pe.kr
|
||||
//
|
||||
// allow if
|
||||
// - user has Admin/Agent-role for all its sub-domains and the direct parent-Domain which are set up at at Hostsharing
|
||||
// - domain has DNS zone with TXT record approval
|
||||
// - parent-domain has DNS zone with TXT record approval
|
||||
//
|
||||
// TXT-Record check:
|
||||
// new InitialDirContext().getAttributes("dns:_netblocks.google.com", new String[] { "TXT"}).get("TXT").getAll();
|
||||
final var violations = // new ArrayList<String>();
|
||||
super.validateEntity(assetEntity);
|
||||
if (!violations.isEmpty()) {
|
||||
return violations;
|
||||
}
|
||||
|
||||
return super.validateEntity(assetEntity);
|
||||
final var domainName = assetEntity.getIdentifier();
|
||||
final var dnsResult = new Dns(domainName).fetchRecordsOfType("TXT");
|
||||
final Supplier<String> getCode = () -> assetEntity.getBookingItem().getDirectValue("verificationCode", String.class);
|
||||
switch (dnsResult.status()) {
|
||||
case Dns.Status.SUCCESS: {
|
||||
final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get();
|
||||
final var verificationFound = findTxtRecord(dnsResult, expectedTxtRecordValue)
|
||||
.or(() -> superDomain(domainName)
|
||||
.flatMap(superDomainName -> findTxtRecord(
|
||||
new Dns(superDomainName).fetchRecordsOfType("TXT"),
|
||||
expectedTxtRecordValue))
|
||||
);
|
||||
if (verificationFound.isEmpty()) {
|
||||
violations.add(
|
||||
"[DNS] no TXT record '" + expectedTxtRecordValue +
|
||||
"' found for domain name '" + domainName + "' (nor in its super-domain)");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Dns.Status.NAME_NOT_FOUND: {
|
||||
hsh-michaelhoennig marked this conversation as resolved
|
||||
if (isDnsVerificationRequiredForUnregisteredDomain(assetEntity)) {
|
||||
final var superDomain = superDomain(domainName);
|
||||
final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get();
|
||||
final var verificationFoundInSuperDomain = superDomain.flatMap(superDomainName -> findTxtRecord(
|
||||
new Dns(superDomainName).fetchRecordsOfType("TXT"),
|
||||
expectedTxtRecordValue));
|
||||
if (verificationFoundInSuperDomain.isEmpty()) {
|
||||
violations.add(
|
||||
"[DNS] no TXT record '" + expectedTxtRecordValue +
|
||||
"' found for domain name '" + superDomain.orElseThrow() + "'");
|
||||
}
|
||||
}
|
||||
// otherwise no DNS verification to be able to setup DNS for domains to register
|
||||
break;
|
||||
}
|
||||
|
||||
case Dns.Status.INVALID_NAME:
|
||||
violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'");
|
||||
break;
|
||||
|
||||
case Dns.Status.SERVICE_UNAVAILABLE:
|
||||
case Dns.Status.UNKNOWN_FAILURE:
|
||||
violations.add("[DNS] lookup failed for domain name '" + assetEntity.getIdentifier() + "': " + dnsResult.exception());
|
||||
break;
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern identifierPattern(final HsHostingAsset assetEntity) {
|
||||
hsh-marcsandlus
commented
findRecord findRecord
|
||||
return identifierPattern;
|
||||
if (assetEntity.getBookingItem() != null) {
|
||||
final var bookingItemDomainName = assetEntity.getBookingItem()
|
||||
.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
|
||||
return Pattern.compile(bookingItemDomainName, Pattern.CASE_INSENSITIVE | Pattern.LITERAL);
|
||||
}
|
||||
final var parentDomainName = assetEntity.getParentAsset().getIdentifier();
|
||||
return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
private static boolean isDnsVerificationRequiredForUnregisteredDomain(final HsHostingAsset assetEntity) {
|
||||
return !Dns.isRegistrableDomain(assetEntity.getIdentifier())
|
||||
&& assetEntity.getParentAsset() == null;
|
||||
}
|
||||
|
||||
|
||||
private static Optional<String> findTxtRecord(final Dns.Result result, final String expectedTxtRecordValue) {
|
||||
return result.records().stream()
|
||||
.filter(r -> r.contains(expectedTxtRecordValue))
|
||||
.findAny();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package net.hostsharing.hsadminng.hs.validation;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.mapper.Array;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -15,11 +17,19 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
|
||||
|
||||
protected static final String[] KEY_ORDER = Array.join(
|
||||
ValidatableProperty.KEY_ORDER_HEAD,
|
||||
Array.of("matchesRegEx", "minLength", "maxLength", "provided"),
|
||||
Array.of("matchesRegEx", "matchesRegExDescription",
|
||||
"notMatchesRegEx", "notMatchesRegExDescription",
|
||||
"minLength", "maxLength",
|
||||
"provided"),
|
||||
ValidatableProperty.KEY_ORDER_TAIL,
|
||||
Array.of("undisclosed"));
|
||||
private String[] provided;
|
||||
private Pattern[] matchesRegEx;
|
||||
private String matchesRegExDescription;
|
||||
private Pattern[] notMatchesRegEx;
|
||||
private String notMatchesRegExDescription;
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private Consumer<String> describedAsConsumer;
|
||||
private Integer minLength;
|
||||
private Integer maxLength;
|
||||
private boolean undisclosed;
|
||||
@ -56,10 +66,23 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
|
||||
|
||||
public P matchesRegEx(final String... regExPattern) {
|
||||
this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new);
|
||||
this.describedAsConsumer = violationMessage -> matchesRegExDescription = violationMessage;
|
||||
return self();
|
||||
}
|
||||
|
||||
/// predifined values, similar to fixed values in a combobox
|
||||
public P notMatchesRegEx(final String... regExPattern) {
|
||||
this.notMatchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new);
|
||||
this.describedAsConsumer = violationMessage -> notMatchesRegExDescription = violationMessage;
|
||||
return self();
|
||||
}
|
||||
|
||||
public P describedAs(final String violationMessage) {
|
||||
describedAsConsumer.accept(violationMessage);
|
||||
describedAsConsumer = null;
|
||||
return self();
|
||||
}
|
||||
|
||||
/// predefined values, similar to fixed values in a combobox
|
||||
public P provided(final String... provided) {
|
||||
this.provided = provided;
|
||||
return self();
|
||||
@ -78,16 +101,10 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
|
||||
@Override
|
||||
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||
super.validate(result, propValue, propProvider);
|
||||
if (minLength != null && propValue.length()<minLength) {
|
||||
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length());
|
||||
}
|
||||
if (maxLength != null && propValue.length()>maxLength) {
|
||||
result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length());
|
||||
}
|
||||
if (matchesRegEx != null &&
|
||||
stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) {
|
||||
result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match" + (matchesRegEx.length>1?" any":""));
|
||||
}
|
||||
validateMinLength(result, propValue);
|
||||
validateMaxLength(result, propValue);
|
||||
validateMatchesRegEx(result, propValue);
|
||||
validateNotMatchesRegEx(result, propValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -99,4 +116,47 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
|
||||
protected String simpleTypeName() {
|
||||
return "string";
|
||||
}
|
||||
|
||||
private void validateMinLength(final List<String> result, final String propValue) {
|
||||
if (minLength != null && propValue.length()<minLength) {
|
||||
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMaxLength(final List<String> result, final String propValue) {
|
||||
if (maxLength != null && propValue.length()>maxLength) {
|
||||
result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMatchesRegEx(final List<String> result, final String propValue) {
|
||||
if (matchesRegEx != null &&
|
||||
stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) {
|
||||
if (matchesRegExDescription != null) {
|
||||
result.add(propertyName + "' = " + display(propValue) + " " + matchesRegExDescription);
|
||||
} else if (matchesRegEx.length>1) {
|
||||
result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) +
|
||||
" but " + display(propValue) + " does not match any");
|
||||
} else {
|
||||
result.add(propertyName + "' is expected to match " + Arrays.toString(matchesRegEx) + " but " + display(
|
||||
propValue) +
|
||||
" does not match");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNotMatchesRegEx(final List<String> result, final String propValue) {
|
||||
if (notMatchesRegEx != null &&
|
||||
stream(notMatchesRegEx).map(p -> p.matcher(propValue)).anyMatch(Matcher::matches)) {
|
||||
if (notMatchesRegExDescription != null) {
|
||||
result.add(propertyName + "' = " + display(propValue) + " " + notMatchesRegExDescription);
|
||||
} else if (notMatchesRegEx.length>1) {
|
||||
result.add(propertyName + "' is expected not to match any of " + Arrays.toString(notMatchesRegEx) +
|
||||
" but " + display(propValue) + " does match at least one");
|
||||
} else {
|
||||
result.add(propertyName + "' is expected not to match " + Arrays.toString(notMatchesRegEx) +
|
||||
" but " + display(propValue) + " does match");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,155 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Map.entry;
|
||||
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP;
|
||||
import static org.apache.commons.lang3.StringUtils.right;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HsDomainSetupBookingItemValidatorUnitTest {
|
||||
|
||||
public static final String TOO_LONG_DOMAIN_NAME = "asdfghijklmnopqrstuvwxyz0123456789.".repeat(8) + "example.org";
|
||||
final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
|
||||
.debitorNumber(12345)
|
||||
.build();
|
||||
final HsBookingProjectRealEntity project = HsBookingProjectRealEntity.builder()
|
||||
.debitor(debitor)
|
||||
.caption("Test-Project")
|
||||
.build();
|
||||
private EntityManager em;
|
||||
|
||||
@Test
|
||||
void acceptsRegisterableDomain() {
|
||||
// given
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.org")
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsMaximumDomainNameLength() {
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253))
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTooLongTotalName() {
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254))
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.domainName' length is expected to be at max 253 but length of 'dfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.asdfghijklmnopqrstuvwxyz0123456789.example.org' is 254");
|
||||
}
|
||||
|
||||
hsh-michaelhoennig marked this conversation as resolved
Outdated
hsh-marcsandlus
commented
+.de +.de
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"de", "com", "net", "org", "actually-any-top-level-domain",
|
||||
"co.uk", "org.uk", "gov.uk", "ac.uk", "sch.uk",
|
||||
"com.au", "net.au", "org.au", "edu.au", "gov.au", "asn.au", "id.au",
|
||||
"co.jp", "ne.jp", "or.jp", "ac.jp", "go.jp",
|
||||
"com.cn", "net.cn", "org.cn", "gov.cn", "edu.cn", "ac.cn",
|
||||
"com.br", "net.br", "org.br", "gov.br", "edu.br", "mil.br", "art.br",
|
||||
"co.in", "net.in", "org.in", "gen.in", "firm.in", "ind.in",
|
||||
"com.mx", "net.mx", "org.mx", "gob.mx", "edu.mx",
|
||||
"gov.it", "edu.it",
|
||||
"co.nz", "net.nz", "org.nz", "govt.nz", "ac.nz", "school.nz", "geek.nz", "kiwi.nz",
|
||||
"co.kr", "ne.kr", "or.kr", "go.kr", "re.kr", "pe.kr"
|
||||
})
|
||||
void rejectRegistrarLevelDomain(final String secondLevelRegistrarDomain) {
|
||||
// given
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", secondLevelRegistrarDomain)
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).contains(
|
||||
"'D-12345:Test-Project:Test-Domain.resources.domainName' = '" +
|
||||
secondLevelRegistrarDomain +
|
||||
"' is a forbidden registrar-level domain name");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"hostsharing.net", "hostsharing.org", "hostsharing.com", "hostsharing.coop", "hostsharing.de"
|
||||
})
|
||||
void rejectHostsharingDomain(final String secondLevelRegistrarDomain) {
|
||||
// given
|
||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.project(project)
|
||||
.caption("Test-Domain")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", secondLevelRegistrarDomain)
|
||||
))
|
||||
.build();
|
||||
|
||||
// when
|
||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactly(
|
||||
"'D-12345:Test-Project:Test-Domain.resources.domainName' = '" +
|
||||
secondLevelRegistrarDomain +
|
||||
"' is a forbidden Hostsharing domain name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsAllValidations() {
|
||||
// when
|
||||
final var validator = HsBookingItemEntityValidatorRegistry.forType(DOMAIN_SETUP);
|
||||
|
||||
// then
|
||||
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, maxLength=253, required=true, writeOnce=true}",
|
||||
"{type=string, propertyName=verificationCode, readOnly=true, computed=IN_INIT}");
|
||||
}
|
||||
}
|
@ -9,10 +9,12 @@ 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;
|
||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.ClassOrderer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Order;
|
||||
@ -64,6 +66,11 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
@Autowired
|
||||
JpaAttempt jpaAttempt;
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
Dns.resetFakeResults();
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Order(2)
|
||||
class ListAssets {
|
||||
@ -249,6 +256,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
void globalAdmin_canAddTopLevelAsset() {
|
||||
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
Dns.fakeResultForDomain("example.com", 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(() ->
|
||||
@ -256,6 +264,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
.project(givenProject)
|
||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||
.caption("some temp domain setup booking item")
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", "example.com")))
|
||||
.build()
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DnsUnitTest {
|
||||
|
||||
@Test
|
||||
void isRegistrarLevelDomain() {
|
||||
assertThat(Dns.isRegistrarLevelDomain("de")).isTrue();
|
||||
assertThat(Dns.isRegistrarLevelDomain("example.de")).isFalse();
|
||||
|
||||
assertThat(Dns.isRegistrarLevelDomain("co.uk")).isTrue();
|
||||
assertThat(Dns.isRegistrarLevelDomain("example.co.uk")).isFalse();
|
||||
assertThat(Dns.isRegistrarLevelDomain("co.uk.com")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isRegistrableDomain() {
|
||||
assertThat(Dns.isRegistrableDomain("de")).isFalse();
|
||||
assertThat(Dns.isRegistrableDomain("example.de")).isTrue();
|
||||
assertThat(Dns.isRegistrableDomain("sub.example.de")).isFalse();
|
||||
|
||||
assertThat(Dns.isRegistrableDomain("co.uk")).isFalse();
|
||||
assertThat(Dns.isRegistrableDomain("example.co.uk")).isTrue();
|
||||
assertThat(Dns.isRegistrableDomain("sub.example.co.uk")).isFalse();
|
||||
}
|
||||
}
|
@ -156,9 +156,9 @@ class HsDomainHttpSetupHostingAssetValidatorUnitTest {
|
||||
// then
|
||||
assertThat(result).containsExactlyInAnyOrder(
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.htdocsfallback' is expected to be of type Boolean, but is of type String",
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.fcgi-php-bin' is expected to match any of [^/.*] but 'false' does not match",
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match any of [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))] but '' does not match",
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match any of [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))] but '@' does not match",
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match any of [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))] but 'example.com' does not match");
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.fcgi-php-bin' is expected to match [^/.*] but 'false' does not match",
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))] but '' does not match",
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))] but '@' does not match",
|
||||
"'DOMAIN_HTTP_SETUP:example.org|HTTP.config.subdomains' is expected to match [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))] but 'example.com' does not match");
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,26 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||
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.HsHostingAssetRealEntity;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.DOMAIN_SETUP;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
||||
@ -17,62 +29,90 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
|
||||
static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder<?, ?> validEntityBuilder() {
|
||||
public static final Dns.Result DOMAIN_NOT_REGISTERED = Dns.Result.fromException(new NameNotFoundException(
|
||||
"domain not registered"));
|
||||
|
||||
static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder<?, ?> validEntityBuilder(
|
||||
final String domainName,
|
||||
final Function<HsBookingItemRealEntity.HsBookingItemRealEntityBuilder<?, ?>, HsBookingItemRealEntity> buildBookingItem) {
|
||||
final HsBookingItemRealEntity bookingItem = buildBookingItem.apply(
|
||||
HsBookingItemRealEntity.builder()
|
||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||
.resources(new HashMap<>(ofEntries(
|
||||
entry("domainName", domainName)
|
||||
))));
|
||||
HsBookingItemEntityValidatorRegistry.forType(HsBookingItemType.DOMAIN_SETUP).prepareProperties(null, bookingItem);
|
||||
return HsHostingAssetRbacEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
.bookingItem(HsBookingItemRealEntity.builder().type(HsBookingItemType.DOMAIN_SETUP).build())
|
||||
.identifier("example.org");
|
||||
.bookingItem(bookingItem)
|
||||
.identifier(domainName);
|
||||
}
|
||||
|
||||
enum InvalidDomainNameIdentifier {
|
||||
EMPTY(""),
|
||||
TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"),
|
||||
DASH_AT_BEGINNING("-example.com"),
|
||||
DOT_AT_BEGINNING(".example.com"),
|
||||
DOT_AT_END("example.com.");
|
||||
static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder<?, ?> validEntityBuilder(final String domainName) {
|
||||
return validEntityBuilder(domainName, HsBookingItemRealEntity.HsBookingItemRealEntityBuilder::build);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
Dns.resetFakeResults();
|
||||
}
|
||||
|
||||
//=====================================================================================================================
|
||||
|
||||
enum InvalidSubDomainNameIdentifierForExampleOrg {
|
||||
IDENTICAL("example.org"),
|
||||
TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.example.org"),
|
||||
DASH_AT_BEGINNING("-sub.example.org"),
|
||||
DOT(".example.org"),
|
||||
DOT_AT_BEGINNING(".sub.example.org"),
|
||||
DOUBLE_DOT("sub..example.com.");
|
||||
|
||||
final String domainName;
|
||||
|
||||
InvalidDomainNameIdentifier(final String domainName) {
|
||||
InvalidSubDomainNameIdentifierForExampleOrg(final String domainName) {
|
||||
this.domainName = domainName;
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(InvalidDomainNameIdentifier.class)
|
||||
void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) {
|
||||
@EnumSource(InvalidSubDomainNameIdentifierForExampleOrg.class)
|
||||
void rejectsInvalidIdentifier(final InvalidSubDomainNameIdentifierForExampleOrg testCase) {
|
||||
// given
|
||||
final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build();
|
||||
final var givenEntity = validEntityBuilder(testCase.domainName)
|
||||
.bookingItem(null)
|
||||
.parentAsset(HsHostingAssetRealEntity.builder().type(DOMAIN_SETUP).identifier("example.org").build())
|
||||
.build();
|
||||
// fakeValidDnsVerification(givenEntity);
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
|
||||
|
||||
// when
|
||||
final var result = validator.validateEntity(givenEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactly(
|
||||
"'identifier' expected to match '^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}', but is '"+testCase.domainName+"'"
|
||||
assertThat(result).contains(
|
||||
hsh-michaelhoennig marked this conversation as resolved
hsh-marcsandlus
commented
test für parentAsset, inkl. gültige Zeichenlänge test für parentAsset, inkl. gültige Zeichenlänge
|
||||
"'identifier' expected to match '(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))\\.example\\.org', but is '" + testCase.domainName + "'"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
enum ValidDomainNameIdentifier {
|
||||
SIMPLE("exampe.org"),
|
||||
MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.de"),
|
||||
WITH_DASH("example-test.com"),
|
||||
SUBDOMAIN("test.example.com");
|
||||
enum ValidSubDomainNameIdentifier {
|
||||
SIMPLE("sub.example.org"),
|
||||
MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.example.org"),
|
||||
MIN_LENGTH("x.example.org"),
|
||||
WITH_DASH("example-test.example.org");
|
||||
|
||||
final String domainName;
|
||||
|
||||
ValidDomainNameIdentifier(final String domainName) {
|
||||
ValidSubDomainNameIdentifier(final String domainName) {
|
||||
this.domainName = domainName;
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(ValidDomainNameIdentifier.class)
|
||||
void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) {
|
||||
@EnumSource(ValidSubDomainNameIdentifier.class)
|
||||
void acceptsValidIdentifier(final ValidSubDomainNameIdentifier testCase) {
|
||||
// given
|
||||
final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build();
|
||||
final var givenEntity = validEntityBuilder(testCase.domainName).identifier(testCase.domainName).build();
|
||||
fakeValidDnsVerification(givenEntity);
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
|
||||
|
||||
// when
|
||||
@ -82,6 +122,13 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
private static void fakeValidDnsVerification(final HsHostingAssetRbacEntity givenEntity) {
|
||||
final var expectedHash = givenEntity.getBookingItem().getDirectValue("verificationCode", String.class);
|
||||
Dns.fakeResultForDomain(
|
||||
givenEntity.getIdentifier(),
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsNoProperties() {
|
||||
// when
|
||||
@ -94,10 +141,48 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
@Test
|
||||
void validatesReferencedEntities() {
|
||||
// given
|
||||
final var domainSetupHostingAssetEntity = validEntityBuilder()
|
||||
final var domainSetupHostingAssetEntity = validEntityBuilder("example.org",
|
||||
bib -> bib.type(HsBookingItemType.CLOUD_SERVER).build())
|
||||
.parentAsset(HsHostingAssetRealEntity.builder().type(CLOUD_SERVER).build())
|
||||
.assignedToAsset(HsHostingAssetRealEntity.builder().type(MANAGED_SERVER).build())
|
||||
.bookingItem(HsBookingItemRealEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build())
|
||||
.build();
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
|
||||
|
||||
// when
|
||||
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).contains(
|
||||
"'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.assignedToAsset' must be null but is of type MANAGED_SERVER");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDomainNameNotMatchingBookingItemDomainName() {
|
||||
// given
|
||||
final var domainSetupHostingAssetEntity = validEntityBuilder("not-matching-booking-item-domain-name.org",
|
||||
bib -> bib.resources(new HashMap<>(ofEntries(
|
||||
entry("domainName", "example.org")
|
||||
))).build()
|
||||
).build();
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
|
||||
|
||||
// when
|
||||
final var result = validator.validateEntity(domainSetupHostingAssetEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactlyInAnyOrder(
|
||||
"'identifier' expected to match 'example.org', but is 'not-matching-booking-item-domain-name.org'");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "not-matching-booking-item-domain-name.org", "indirect.subdomain.example.org" })
|
||||
void rejectsDomainNameWhichIsNotADirectSubdomainOfParentAsset(final String newDomainName) {
|
||||
// given
|
||||
final var domainSetupHostingAssetEntity = validEntityBuilder(newDomainName)
|
||||
.bookingItem(null)
|
||||
.parentAsset(createValidParentDomainSetupAsset("example.org"))
|
||||
.build();
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
|
||||
|
||||
@ -106,21 +191,248 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactlyInAnyOrder(
|
||||
"'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.assignedToAsset' must be null but is of type MANAGED_SERVER");
|
||||
"'identifier' expected to match '(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))\\.example\\.org', " +
|
||||
"but is '" + newDomainName + "'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void expectsEitherParentAssetOrBookingItem() {
|
||||
void rejectsIfNeitherBookingItemNorParentAssetAreSet() {
|
||||
|
||||
// given
|
||||
final var domainSetupHostingAssetEntity = validEntityBuilder().build();
|
||||
final var domainSetupHostingAssetEntity = validEntityBuilder("example.org")
|
||||
.bookingItem(null)
|
||||
.parentAsset(null)
|
||||
.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).containsExactly("'DOMAIN_SETUP:example.org.bookingItem' must be of type DOMAIN_SETUP but is null");
|
||||
}
|
||||
|
||||
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("example.org").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 allowSetupOfAvailableRegistrableDomain() {
|
||||
domainSetupFor("example.com").notRegistered()
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowSetupOfAvailableRegistrable2ndLevelDomain() {
|
||||
domainSetupFor("example.co.uk").notRegistered()
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectSetupOfRegisteredRegistrable2ndLevelDomainWithoutVerification() {
|
||||
domainSetupFor("example.co.uk").registered()
|
||||
.isRejectedWithCauseMissingVerificationIn("example.co.uk");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowSetupOfRegisteredRegistrable2ndLevelDomainWithVerification() {
|
||||
domainSetupFor("example.co.uk").registeredWithVerification()
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectSetupOfExistingRegistrableDomainWithoutValidDnsVerification() {
|
||||
domainSetupFor("example.com").registered()
|
||||
.isRejectedWithCauseMissingVerificationIn("example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowSetupOfExistingRegistrableDomainWithValidDnsVerification() {
|
||||
domainSetupFor("example.org").registeredWithVerification()
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowSetupOfUnregisteredSubdomainWithValidDnsVerificationInSuperDomain() {
|
||||
domainSetupFor("sub.example.org").notRegistered().withVerificationIn("example.org")
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectSetupOfExistingRegistrableDomainWithInvalidDnsVerification() {
|
||||
domainSetupFor("example.com").registeredWithInvalidVerification()
|
||||
.isRejectedWithCauseMissingVerificationIn("example.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptSetupOfRegisteredSubdomainWithInvalidDnsVerificationButValidDnsVerificationInSuperDomain() {
|
||||
domainSetupFor("sub.example.com").registeredWithInvalidVerification().withVerificationIn("example.com")
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectSetupOfUnregisteredSubdomainWithoutParentAssetAndWithoutDnsVerificationInSuperDomain() {
|
||||
domainSetupFor("sub.example.org").notRegistered()
|
||||
.isRejectedWithCauseMissingVerificationIn("example.org");
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptSetupOfUnregisteredSubdomainWithParentAssetEvenWithoutDnsVerificationInSuperDomain() {
|
||||
domainSetupWithParentAssetFor("sub.example.org").notRegistered()
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowSetupOfExistingSubdomainWithValidDnsVerificationInSuperDomain() {
|
||||
domainSetupFor("sub.example.org").registered()
|
||||
.withVerificationIn("example.org")
|
||||
.isAccepted();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectSetupOfExistingSubdomainWithoutDnsVerification() {
|
||||
domainSetupFor("sub.example.org").registered()
|
||||
.isRejectedWithCauseMissingVerificationIn("sub.example.org");
|
||||
}
|
||||
|
||||
//====================================================================================================================
|
||||
|
||||
private static HsHostingAssetRealEntity createValidParentDomainSetupAsset(final String parentDomainName) {
|
||||
final var bookingItem = HsBookingItemRealEntity.builder()
|
||||
.type(HsBookingItemType.DOMAIN_SETUP)
|
||||
.resources(ofEntries(
|
||||
entry("domainName", parentDomainName)
|
||||
))
|
||||
.build();
|
||||
final var parentAsset = HsHostingAssetRealEntity.builder()
|
||||
.type(DOMAIN_SETUP)
|
||||
hsh-marcsandlus
commented
DomainSetup DomainSetup
|
||||
.bookingItem(bookingItem)
|
||||
.identifier(parentDomainName).build();
|
||||
return parentAsset;
|
||||
}
|
||||
|
||||
class DomainSetupBuilder {
|
||||
|
||||
private final HsHostingAssetRbacEntity domainAsset;
|
||||
private final String expectedHash;
|
||||
|
||||
public DomainSetupBuilder(final String domainName) {
|
||||
domainAsset = validEntityBuilder(domainName).build();
|
||||
expectedHash = domainAsset.getBookingItem().getDirectValue("verificationCode", String.class);
|
||||
}
|
||||
|
||||
public DomainSetupBuilder(final HsHostingAssetRealEntity parentAsset, final String domainName) {
|
||||
domainAsset = validEntityBuilder(domainName)
|
||||
.bookingItem(null)
|
||||
.parentAsset(parentAsset)
|
||||
.build();
|
||||
expectedHash = null;
|
||||
}
|
||||
|
||||
DomainSetupBuilder notRegistered() {
|
||||
Dns.fakeResultForDomain(domainAsset.getIdentifier(), DOMAIN_NOT_REGISTERED);
|
||||
return this;
|
||||
}
|
||||
|
||||
DomainSetupBuilder registered() {
|
||||
Dns.fakeResultForDomain(
|
||||
domainAsset.getIdentifier(),
|
||||
Dns.Result.fromRecords());
|
||||
return this;
|
||||
}
|
||||
|
||||
DomainSetupBuilder registeredWithInvalidVerification() {
|
||||
Dns.fakeResultForDomain(
|
||||
domainAsset.getIdentifier(),
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=SOME-DEFINITELY-WRONG-HASH"));
|
||||
return this;
|
||||
}
|
||||
|
||||
DomainSetupBuilder registeredWithVerification() {
|
||||
withVerificationIn(domainAsset.getIdentifier());
|
||||
return this;
|
||||
}
|
||||
|
||||
DomainSetupBuilder withVerificationIn(final String domainName) {
|
||||
assertThat(expectedHash).as("no expectedHash available").isNotNull();
|
||||
Dns.fakeResultForDomain(
|
||||
domainName,
|
||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=" + expectedHash));
|
||||
return this;
|
||||
}
|
||||
|
||||
void isRejectedWithCauseMissingVerificationIn(final String domainName) {
|
||||
assertThat(expectedHash).as("no expectedHash available").isNotNull();
|
||||
assertThat(validate()).containsAnyOf(
|
||||
"[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash
|
||||
+ "' found for domain name '" + domainName + "' (nor in its super-domain)",
|
||||
"[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=" + expectedHash
|
||||
+ "' found for domain name '" + domainName + "'");
|
||||
}
|
||||
|
||||
void isAccepted() {
|
||||
assertThat(validate()).isEmpty();
|
||||
}
|
||||
|
||||
private List<String> validate() {
|
||||
final var validator = HostingAssetEntityValidatorRegistry.forType(DOMAIN_SETUP);
|
||||
return validator.validateEntity(domainAsset);
|
||||
}
|
||||
}
|
||||
|
||||
private DomainSetupBuilder domainSetupFor(final String domainName) {
|
||||
return new DomainSetupBuilder(domainName);
|
||||
}
|
||||
|
||||
private DomainSetupBuilder domainSetupWithParentAssetFor(final String domainName) {
|
||||
return new DomainSetupBuilder(
|
||||
HsHostingAssetRealEntity.builder().type(DOMAIN_SETUP).identifier(Dns.superDomain(domainName).orElseThrow()).build(),
|
||||
domainName);
|
||||
}
|
||||
}
|
||||
|
@ -89,8 +89,8 @@ class HsEMailAddressHostingAssetValidatorUnitTest {
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactlyInAnyOrder(
|
||||
"'EMAIL_ADDRESS:old-local-part@example.org.config.local-part' is expected to match any of [^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$] but 'no@allowed' does not match",
|
||||
"'EMAIL_ADDRESS:old-local-part@example.org.config.sub-domain' is expected to match any of [^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$] but 'no@allowedeither' does not match",
|
||||
"'EMAIL_ADDRESS:old-local-part@example.org.config.local-part' is expected to match [^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$] but 'no@allowed' does not match",
|
||||
"'EMAIL_ADDRESS:old-local-part@example.org.config.sub-domain' is expected to match [^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$] but 'no@allowedeither' does not match",
|
||||
"'EMAIL_ADDRESS:old-local-part@example.org.config.target' is expected to match any of [^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9][a-z0-9\\.+_-]*)?$, ^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+)?@[a-zA-Z0-9.-]+$, ^nobody$, ^/dev/null$] but 'garbage' does not match any");
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
|
||||
"'UNIX_USER:abc00-temp.config.HDD hard quota' is expected to be at most 0 but is 100",
|
||||
"'UNIX_USER:abc00-temp.config.HDD soft quota' is expected to be at most 100 but is 200",
|
||||
"'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'",
|
||||
"'UNIX_USER:abc00-temp.config.totpKey' is expected to match any of [^0x([0-9A-Fa-f]{2})+$] but provided value does not match",
|
||||
"'UNIX_USER:abc00-temp.config.totpKey' is expected to match [^0x([0-9A-Fa-f]{2})+$] but provided value does not match",
|
||||
"'UNIX_USER:abc00-temp.config.password' length is expected to be at min 8 but length of provided value is 5",
|
||||
"'UNIX_USER:abc00-temp.config.password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters"
|
||||
);
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -1352,7 +1352,7 @@ public class ImportHostingAssets extends BaseOfficeDataImport {
|
||||
}
|
||||
|
||||
private void importDatabaseUsers(final String[] header, final List<String[]> records) {
|
||||
HashGenerator.enableChouldBeHash(true);
|
||||
HashGenerator.enableCouldBeHash(true);
|
||||
final var columns = new Columns(header);
|
||||
records.stream()
|
||||
.map(this::trimAll)
|
||||
@ -1552,6 +1552,8 @@ public class ImportHostingAssets extends BaseOfficeDataImport {
|
||||
.caption("BI " + domainSetup.getIdentifier())
|
||||
.project((HsBookingProjectRealEntity) relatedProject)
|
||||
//.validity(toPostgresDateRange(created, cancelled))
|
||||
.resources(Map.ofEntries(
|
||||
entry("domainName", domainSetup.getIdentifier())))
|
||||
.build();
|
||||
domainSetup.setBookingItem(bookingItem);
|
||||
bookingItems.put(nextAvailableBookingItemId(), bookingItem);
|
||||
|
Loading…
Reference in New Issue
Block a user
use getParentAsset == null + Test