check-domain-setup-permission (#97)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #97
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-09-10 13:15:03 +02:00
parent 8e02610679
commit a7d586f0f7
13 changed files with 884 additions and 84 deletions

View File

@ -1,10 +1,59 @@
package net.hostsharing.hsadminng.hs.booking.item.validators; package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
import net.hostsharing.hsadminng.hs.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 { 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() { HsDomainSetupBookingItemValidator() {
super( 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();
}
} }

View File

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

View File

@ -3,55 +3,104 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern; import java.util.regex.Pattern;
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.validators.Dns.superDomain;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainHttpSetupHostingAssetValidator.SUBDOMAIN_NAME_REGEX;
class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}"; 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";
private final Pattern identifierPattern;
HsDomainSetupHostingAssetValidator() { HsDomainSetupHostingAssetValidator() {
super( DOMAIN_SETUP, super(
DOMAIN_SETUP,
AlarmContact.isOptional(), AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES); NO_EXTRA_PROPERTIES);
this.identifierPattern = Pattern.compile(FQDN_REGEX);
} }
@Override @Override
public List<String> validateEntity(final HsHostingAsset assetEntity) { public List<String> validateEntity(final HsHostingAsset assetEntity) {
// TODO.impl: for newly created entities, check the permission of setting up a domain final var violations = // new ArrayList<String>();
// super.validateEntity(assetEntity);
// reject, if the domain is any of these: if (!violations.isEmpty()) {
// hostsharing.com|net|org|coop, // just to be on the safe side return violations;
// [^.}+, // 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();
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: {
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 @Override
protected Pattern identifierPattern(final HsHostingAsset assetEntity) { protected Pattern identifierPattern(final HsHostingAsset assetEntity) {
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();
} }
} }

View File

@ -1,10 +1,12 @@
package net.hostsharing.hsadminng.hs.validation; package net.hostsharing.hsadminng.hs.validation;
import lombok.AccessLevel;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.mapper.Array;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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( protected static final String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_HEAD,
Array.of("matchesRegEx", "minLength", "maxLength", "provided"), Array.of("matchesRegEx", "matchesRegExDescription",
"notMatchesRegEx", "notMatchesRegExDescription",
"minLength", "maxLength",
"provided"),
ValidatableProperty.KEY_ORDER_TAIL, ValidatableProperty.KEY_ORDER_TAIL,
Array.of("undisclosed")); Array.of("undisclosed"));
private String[] provided; private String[] provided;
private Pattern[] matchesRegEx; private Pattern[] matchesRegEx;
private String matchesRegExDescription;
private Pattern[] notMatchesRegEx;
private String notMatchesRegExDescription;
@Setter(AccessLevel.PRIVATE)
private Consumer<String> describedAsConsumer;
private Integer minLength; private Integer minLength;
private Integer maxLength; private Integer maxLength;
private boolean undisclosed; private boolean undisclosed;
@ -56,10 +66,23 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
public P matchesRegEx(final String... regExPattern) { public P matchesRegEx(final String... regExPattern) {
this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new); this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new);
this.describedAsConsumer = violationMessage -> matchesRegExDescription = violationMessage;
return self(); 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) { public P provided(final String... provided) {
this.provided = provided; this.provided = provided;
return self(); return self();
@ -78,16 +101,10 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
@Override @Override
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) { protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
super.validate(result, propValue, propProvider); super.validate(result, propValue, propProvider);
if (minLength != null && propValue.length()<minLength) { validateMinLength(result, propValue);
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length()); validateMaxLength(result, propValue);
} validateMatchesRegEx(result, propValue);
if (maxLength != null && propValue.length()>maxLength) { validateNotMatchesRegEx(result, propValue);
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":""));
}
} }
@Override @Override
@ -99,4 +116,47 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
protected String simpleTypeName() { protected String simpleTypeName() {
return "string"; 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");
}
}
}
} }

View File

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

View File

@ -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.HsBookingItemRealRepository;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository; 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.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
@ -64,6 +66,11 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Autowired @Autowired
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
@AfterEach
void cleanup() {
Dns.resetFakeResults();
}
@Nested @Nested
@Order(2) @Order(2)
class ListAssets { class ListAssets {
@ -249,6 +256,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
void globalAdmin_canAddTopLevelAsset() { void globalAdmin_canAddTopLevelAsset() {
context.define("superuser-alex@hostsharing.net"); 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() final var givenProject = realProjectRepo.findByCaption("D-1000111 default project").stream()
.findAny().orElseThrow(); .findAny().orElseThrow();
final var bookingItem = givenSomeTemporaryBookingItem(() -> final var bookingItem = givenSomeTemporaryBookingItem(() ->
@ -256,6 +264,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.project(givenProject) .project(givenProject)
.type(HsBookingItemType.DOMAIN_SETUP) .type(HsBookingItemType.DOMAIN_SETUP)
.caption("some temp domain setup booking item") .caption("some temp domain setup booking item")
.resources(Map.ofEntries(
entry("domainName", "example.com")))
.build() .build()
); );

View File

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

View File

@ -156,9 +156,9 @@ class HsDomainHttpSetupHostingAssetValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( 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.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.fcgi-php-bin' is expected to match [^/.*] 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 [(\\*|(?!-)[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 [(\\*|(?!-)[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.subdomains' is expected to match [(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))] but 'example.com' does not match");
} }
} }

View File

@ -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.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 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.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.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;
@ -17,62 +29,90 @@ import static org.assertj.core.api.Assertions.assertThat;
class HsDomainSetupHostingAssetValidatorUnitTest { 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() return HsHostingAssetRbacEntity.builder()
.type(DOMAIN_SETUP) .type(DOMAIN_SETUP)
.bookingItem(HsBookingItemRealEntity.builder().type(HsBookingItemType.DOMAIN_SETUP).build()) .bookingItem(bookingItem)
.identifier("example.org"); .identifier(domainName);
} }
enum InvalidDomainNameIdentifier { static HsHostingAssetRbacEntity.HsHostingAssetRbacEntityBuilder<?, ?> validEntityBuilder(final String domainName) {
EMPTY(""), return validEntityBuilder(domainName, HsBookingItemRealEntity.HsBookingItemRealEntityBuilder::build);
TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"), }
DASH_AT_BEGINNING("-example.com"),
DOT_AT_BEGINNING(".example.com"), @AfterEach
DOT_AT_END("example.com."); 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; final String domainName;
InvalidDomainNameIdentifier(final String domainName) { InvalidSubDomainNameIdentifierForExampleOrg(final String domainName) {
this.domainName = domainName; this.domainName = domainName;
} }
} }
@ParameterizedTest @ParameterizedTest
@EnumSource(InvalidDomainNameIdentifier.class) @EnumSource(InvalidSubDomainNameIdentifierForExampleOrg.class)
void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) { void rejectsInvalidIdentifier(final InvalidSubDomainNameIdentifierForExampleOrg testCase) {
// given // 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()); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
// when // when
final var result = validator.validateEntity(givenEntity); final var result = validator.validateEntity(givenEntity);
// then // then
assertThat(result).containsExactly( assertThat(result).contains(
"'identifier' expected to match '^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}', but is '"+testCase.domainName+"'" "'identifier' expected to match '(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))\\.example\\.org', but is '" + testCase.domainName + "'"
); );
} }
enum ValidSubDomainNameIdentifier {
enum ValidDomainNameIdentifier { SIMPLE("sub.example.org"),
SIMPLE("exampe.org"), MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.example.org"),
MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.de"), MIN_LENGTH("x.example.org"),
WITH_DASH("example-test.com"), WITH_DASH("example-test.example.org");
SUBDOMAIN("test.example.com");
final String domainName; final String domainName;
ValidDomainNameIdentifier(final String domainName) { ValidSubDomainNameIdentifier(final String domainName) {
this.domainName = domainName; this.domainName = domainName;
} }
} }
@ParameterizedTest @ParameterizedTest
@EnumSource(ValidDomainNameIdentifier.class) @EnumSource(ValidSubDomainNameIdentifier.class)
void acceptsValidIdentifier(final ValidDomainNameIdentifier testCase) { void acceptsValidIdentifier(final ValidSubDomainNameIdentifier testCase) {
// given // 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()); final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
// when // when
@ -82,6 +122,13 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
assertThat(result).isEmpty(); 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 @Test
void containsNoProperties() { void containsNoProperties() {
// when // when
@ -94,10 +141,48 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
@Test @Test
void validatesReferencedEntities() { void validatesReferencedEntities() {
// given // 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()) .parentAsset(HsHostingAssetRealEntity.builder().type(CLOUD_SERVER).build())
.assignedToAsset(HsHostingAssetRealEntity.builder().type(MANAGED_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(); .build();
final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType()); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
@ -106,21 +191,248 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'DOMAIN_SETUP:example.org.bookingItem' or parentItem must be null but is of type CLOUD_SERVER", "'identifier' expected to match '(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))\\.example\\.org', " +
"'DOMAIN_SETUP:example.org.parentAsset' must be null or of type DOMAIN_SETUP but is of type CLOUD_SERVER", "but is '" + newDomainName + "'");
"'DOMAIN_SETUP:example.org.assignedToAsset' must be null but is of type MANAGED_SERVER");
} }
@Test @Test
void expectsEitherParentAssetOrBookingItem() { void rejectsIfNeitherBookingItemNorParentAssetAreSet() {
// given // 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()); final var validator = HostingAssetEntityValidatorRegistry.forType(domainSetupHostingAssetEntity.getType());
// when // when
final var result = validator.validateEntity(domainSetupHostingAssetEntity); final var result = validator.validateEntity(domainSetupHostingAssetEntity);
// then // 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(); 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)
.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);
}
} }

View File

@ -89,8 +89,8 @@ class HsEMailAddressHostingAssetValidatorUnitTest {
// then // then
assertThat(result).containsExactlyInAnyOrder( 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.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 any of [^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$] but 'no@allowedeither' 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"); "'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");
} }

View File

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

View File

@ -69,7 +69,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
512167, // 11139, partner without contractual contact 512167, // 11139, partner without contractual contact
512170, // 11142, partner without contractual contact 512170, // 11142, partner without contractual contact
511725, // 10764, 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 -1
); );

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)
@ -1552,6 +1552,8 @@ public class ImportHostingAssets extends BaseOfficeDataImport {
.caption("BI " + domainSetup.getIdentifier()) .caption("BI " + domainSetup.getIdentifier())
.project((HsBookingProjectRealEntity) relatedProject) .project((HsBookingProjectRealEntity) relatedProject)
//.validity(toPostgresDateRange(created, cancelled)) //.validity(toPostgresDateRange(created, cancelled))
.resources(Map.ofEntries(
entry("domainName", domainSetup.getIdentifier())))
.build(); .build();
domainSetup.setBookingItem(bookingItem); domainSetup.setBookingItem(bookingItem);
bookingItems.put(nextAvailableBookingItemId(), bookingItem); bookingItems.put(nextAvailableBookingItemId(), bookingItem);