check-domain-setup-permission #97
@ -1,10 +1,33 @@
|
|||||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
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 {
|
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() {
|
HsDomainSetupBookingItemValidator() {
|
||||||
super(
|
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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,16 @@ 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", "notMatchesRegEx", "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 +63,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 +98,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 +113,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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]}");
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user