disallow domain setup for top-level and registrar-level domains

This commit is contained in:
Michael Hoennig 2024-09-05 11:05:06 +02:00
parent fbd17a21e2
commit 9cbdb1fc47
6 changed files with 217 additions and 20 deletions

View File

@ -1,10 +1,33 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.mapper.Array;
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[] REGISRTAR_LEVEL_DOMAINS = Array.of(
// "[^.]+", // top-level-domains are already rejected by FQDN_REGEX
"(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"
);
// hostsharing.com|net|org|coop, // just to be on the safe side
HsDomainSetupBookingItemValidator() {
super(
// no properties yet. maybe later, the setup code goes here?
stringProperty("domainName").writeOnce()
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
.notMatchesRegEx(REGISRTAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
.required()
);
}
}

View File

@ -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,16 @@ 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", "notMatchesRegEx", "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 +63,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 +98,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 +113,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");
}
}
}
}

View File

@ -0,0 +1,117 @@
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.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.DOMAIN_SETUP;
import static org.assertj.core.api.Assertions.assertThat;
class HsDomainSetupBookingItemValidatorUnitTest {
final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder()
.debitorNumber(12345)
.build();
final HsBookingProjectRealEntity project = HsBookingProjectRealEntity.builder()
.debitor(debitor)
.caption("Test-Project")
.build();
private EntityManager em;
@Test
void acceptsUnregisteredDomain() {
// given
final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder()
.type(DOMAIN_SETUP)
.project(project)
.caption("Test-Domain")
.resources(Map.ofEntries(
entry("domainName", "example.org") // TODO.test: amend once we check registration
))
.build();
// when
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity);
// then
assertThat(result).isEmpty();
}
@Test
void rejectsTopLevelDomain() {
// given
final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder()
.type(DOMAIN_SETUP)
.project(project)
.caption("Test-Domain")
.resources(Map.ofEntries(
entry("domainName", "org")
))
.build();
// when
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity);
// then
assertThat(result).containsExactly("'D-12345:Test-Project:Test-Domain.resources.domainName' = 'org' is not a (non-top-level) fully qualified domain name");
}
@ParameterizedTest
@ValueSource(strings = {
"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 reject2ndLevelRegistrarDomain(final String secondLevelRegistrarDomain) {
// given
final var cloudServerBookingItemEntity = HsBookingItemRealEntity.builder()
.type(DOMAIN_SETUP)
.project(project)
.caption("Test-Domain")
.resources(Map.ofEntries(
entry("domainName", secondLevelRegistrarDomain)
))
.build();
// when
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, cloudServerBookingItemEntity);
// then
assertThat(result).containsExactly(
"'D-12345:Test-Project:Test-Domain.resources.domainName' = '" +
secondLevelRegistrarDomain +
"' is a forbidden registrar-level domain name");
}
@Test
void containsAllValidations() {
// when
final var validator = HsBookingItemEntityValidatorRegistry.forType(CLOUD_SERVER);
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=boolean, propertyName=active, defaultValue=true}",
"{type=integer, propertyName=CPU, min=1, max=32, required=true}",
"{type=integer, propertyName=RAM, unit=GB, min=1, max=8192, required=true}",
"{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, requiresAtLeastOneOf=[SDD, HDD]}",
"{type=integer, propertyName=HDD, unit=GB, min=250, max=4000, step=250, requiresAtLeastOneOf=[SSD, HDD]}",
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}",
"{type=integer, propertyName=Bandwidth, unit=GB, min=250, max=10000, step=250, requiresAtMaxOneOf=[Bandwidth, Traffic]}",
"{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}");
}
}

View File

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

View File

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

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