1
0

add-domain-http-setup-validation (#73)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/73
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-07-10 15:54:02 +02:00
parent afb6771ed7
commit 0af389d7c6
26 changed files with 363 additions and 81 deletions

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityProcessor;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
import net.hostsharing.hsadminng.context.Context;
@ -72,7 +72,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
final var entity = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var mapped = new HsHostingAssetEntityProcessor(entity)
final var mapped = new HostingAssetEntitySaveProcessor(entity)
.preprocessEntity()
.validateEntity()
.prepareForSave()
@ -133,7 +133,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
new HsHostingAssetEntityPatcher(em, entity).apply(body);
final var mapped = new HsHostingAssetEntityProcessor(entity)
final var mapped = new HostingAssetEntitySaveProcessor(entity)
.preprocessEntity()
.validateEntity()
.prepareForSave()
@ -161,6 +161,6 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@SuppressWarnings("unchecked")
final BiConsumer<HsHostingAssetEntity, HsHostingAssetResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource)
-> resource.setConfig(HsHostingAssetEntityValidatorRegistry.forType(entity.getType())
-> resource.setConfig(HostingAssetEntityValidatorRegistry.forType(entity.getType())
.revampProperties(entity, (Map<String, Object>) resource.getConfig()));
}

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import org.springframework.http.ResponseEntity;
@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override
public ResponseEntity<List<String>> listAssetTypes() {
final var resource = HsHostingAssetEntityValidatorRegistry.types().stream()
final var resource = HostingAssetEntityValidatorRegistry.types().stream()
.map(Enum::name)
.toList();
return ResponseEntity.ok(resource);
@ -26,7 +26,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
final HsHostingAssetTypeResource assetType) {
final Enum<HsHostingAssetType> type = HsHostingAssetType.of(assetType);
final var propValidators = HsHostingAssetEntityValidatorRegistry.forType(type);
final var propValidators = HostingAssetEntityValidatorRegistry.forType(type);
final List<Map<String, Object>> resource = propValidators.properties();
return ResponseEntity.ok(toListOfObjects(resource));
}

View File

@ -11,27 +11,27 @@ import java.util.function.Function;
/**
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsHostingAssetEntity into a readable API.
*/
public class HsHostingAssetEntityProcessor {
public class HostingAssetEntitySaveProcessor {
private final HsEntityValidator<HsHostingAssetEntity> validator;
private String expectedStep = "preprocessEntity";
private HsHostingAssetEntity entity;
private HsHostingAssetResource resource;
public HsHostingAssetEntityProcessor(final HsHostingAssetEntity entity) {
public HostingAssetEntitySaveProcessor(final HsHostingAssetEntity entity) {
this.entity = entity;
this.validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType());
this.validator = HostingAssetEntityValidatorRegistry.forType(entity.getType());
}
/// initial step allowing to set default values before any validations
public HsHostingAssetEntityProcessor preprocessEntity() {
public HostingAssetEntitySaveProcessor preprocessEntity() {
step("preprocessEntity", "validateEntity");
validator.preprocessEntity(entity);
return this;
}
/// validates the entity itself including its properties
public HsHostingAssetEntityProcessor validateEntity() {
public HostingAssetEntitySaveProcessor validateEntity() {
step("validateEntity", "prepareForSave");
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
return this;
@ -39,27 +39,27 @@ public class HsHostingAssetEntityProcessor {
/// hashing passwords etc.
@SuppressWarnings("unchecked")
public HsHostingAssetEntityProcessor prepareForSave() {
public HostingAssetEntitySaveProcessor prepareForSave() {
step("prepareForSave", "saveUsing");
validator.prepareProperties(entity);
return this;
}
public HsHostingAssetEntityProcessor saveUsing(final Function<HsHostingAssetEntity, HsHostingAssetEntity> saveFunction) {
public HostingAssetEntitySaveProcessor saveUsing(final Function<HsHostingAssetEntity, HsHostingAssetEntity> saveFunction) {
step("saveUsing", "validateContext");
entity = saveFunction.apply(entity);
return this;
}
/// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits)
public HsHostingAssetEntityProcessor validateContext() {
public HostingAssetEntitySaveProcessor validateContext() {
step("validateContext", "mapUsing");
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
return this;
}
/// maps entity to JSON resource representation
public HsHostingAssetEntityProcessor mapUsing(
public HostingAssetEntitySaveProcessor mapUsing(
final Function<HsHostingAssetEntity, HsHostingAssetResource> mapFunction) {
step("mapUsing", "revampProperties");
resource = mapFunction.apply(entity);

View File

@ -21,16 +21,16 @@ import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
public abstract class HostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
static final ValidatableProperty<?, ?>[] NO_EXTRA_PROPERTIES = new ValidatableProperty<?, ?>[0];
private final ReferenceValidator<HsBookingItemEntity, HsBookingItemType> bookingItemReferenceValidation;
private final ReferenceValidator<HsHostingAssetEntity, HsHostingAssetType> parentAssetReferenceValidation;
private final ReferenceValidator<HsHostingAssetEntity, HsHostingAssetType> assignedToAssetReferenceValidation;
private final HsHostingAssetEntityValidator.AlarmContact alarmContactValidation;
private final HostingAssetEntityValidator.AlarmContact alarmContactValidation;
HsHostingAssetEntityValidator(
HostingAssetEntityValidator(
final HsHostingAssetType assetType,
final AlarmContact alarmContactValidation,
final ValidatableProperty<?, ?>... properties) {
@ -98,7 +98,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
return assetEntity != null
? enrich(
prefix(assetEntity.toShortString(), "parentAsset"),
HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity))
HostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity))
: emptyList();
}

View File

@ -10,7 +10,7 @@ import java.util.*;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.*;
public class HsHostingAssetEntityValidatorRegistry {
public class HostingAssetEntityValidatorRegistry {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity>> validators = new HashMap<>();
static {
@ -22,6 +22,7 @@ public class HsHostingAssetEntityValidatorRegistry {
register(EMAIL_ALIAS, new HsEMailAliasHostingAssetValidator());
register(DOMAIN_SETUP, new HsDomainSetupHostingAssetValidator());
register(DOMAIN_DNS_SETUP, new HsDomainDnsSetupHostingAssetValidator());
register(DOMAIN_HTTP_SETUP, new HsDomainHttpSetupHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {

View File

@ -6,7 +6,7 @@ import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
class HsCloudServerHostingAssetValidator extends HsHostingAssetEntityValidator {
class HsCloudServerHostingAssetValidator extends HostingAssetEntityValidator {
HsCloudServerHostingAssetValidator() {
super(

View File

@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidator {
class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
// according to RFC 1035 (section 5) and RFC 1034
static final String RR_REGEX_NAME = "([a-z0-9\\.-]+|@)\\s+";

View File

@ -0,0 +1,56 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP;
import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsDomainHttpSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String IDENTIFIER_SUFFIX = "|HTTP";
public static final String FILESYSTEM_PATH = "^/";
public static final String PARTIAL_DOMAIN_NAME_REGEX = "(?!-)[A-Za-z0-9-]{1,63}(?<!-)";
HsDomainHttpSetupHostingAssetValidator() {
super(
DOMAIN_HTTP_SETUP,
AlarmContact.isOptional(),
booleanProperty("htdocsfallback").withDefault(true),
booleanProperty("indexes").withDefault(true),
booleanProperty("cgi").withDefault(true),
booleanProperty("passenger").withDefault(true),
booleanProperty("passenger-errorpage").withDefault(false),
booleanProperty("fastcgi").withDefault(true),
booleanProperty("autoconfig").withDefault(true),
booleanProperty("greylisting").withDefault(true),
booleanProperty("includes").withDefault(true),
booleanProperty("letsencrypt").withDefault(true),
booleanProperty("multiviews").withDefault(true),
stringProperty("fcgi-php-bin").matchesRegEx(FILESYSTEM_PATH).provided("/usr/lib/cgi-bin/php").withDefault("/usr/lib/cgi-bin/php"),
stringProperty("passenger-nodejs").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/node").withDefault("/usr/bin/node"),
stringProperty("passenger-python").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/python3").withDefault("/usr/bin/python3"),
stringProperty("passenger-ruby").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/ruby").withDefault("/usr/bin/ruby"),
arrayOf(
stringProperty("subdomains").matchesRegEx(PARTIAL_DOMAIN_NAME_REGEX).required()
).optional());
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + Pattern.quote(IDENTIFIER_SUFFIX) + "$");
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX));
}
}
}

View File

@ -7,9 +7,9 @@ import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP;
class HsDomainSetupHostingAssetValidator extends HsHostingAssetEntityValidator {
class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String DOMAIN_NAME_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}";
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}";
private final Pattern identifierPattern;
@ -18,7 +18,7 @@ class HsDomainSetupHostingAssetValidator extends HsHostingAssetEntityValidator {
AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES);
this.identifierPattern = Pattern.compile(DOMAIN_NAME_REGEX);
this.identifierPattern = Pattern.compile(FQDN_REGEX);
}
@Override

View File

@ -8,7 +8,7 @@ import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsEMailAliasHostingAssetValidator extends HsHostingAssetEntityValidator {
class HsEMailAliasHostingAssetValidator extends HostingAssetEntityValidator {
private static final String UNIX_USER_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$"; // also accepts legacy pac-names
private static final String EMAIL_ADDRESS_REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"; // RFC 5322

View File

@ -9,7 +9,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator {
class HsManagedServerHostingAssetValidator extends HostingAssetEntityValidator {
public HsManagedServerHostingAssetValidator() {
super(

View File

@ -6,7 +6,7 @@ import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator {
class HsManagedWebspaceHostingAssetValidator extends HostingAssetEntityValidator {
public HsManagedWebspaceHostingAssetValidator() {
super(
MANAGED_WEBSPACE,

View File

@ -12,7 +12,7 @@ import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerPro
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
class HsUnixUserHostingAssetValidator extends HostingAssetEntityValidator {
private static final int DASH_LENGTH = "-".length();

View File

@ -15,9 +15,10 @@ 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"),
Array.of("matchesRegEx", "minLength", "maxLength", "provided"),
ValidatableProperty.KEY_ORDER_TAIL,
Array.of("undisclosed"));
private String[] provided;
private Pattern[] matchesRegEx;
private Integer minLength;
private Integer maxLength;
@ -50,6 +51,12 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
return self();
}
/// predifined values, similar to fixed values in a combobox
public P provided(final String... provided) {
this.provided = provided;
return self();
}
/**
* The property value is not disclosed in error messages.
*
@ -70,7 +77,7 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
}
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 any");
result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match" + (matchesRegEx.length>1?" any":""));
}
if (isReadOnly() && propValue != null) {
result.add(propertyName + "' is readonly but given as " + display(propValue));

View File

@ -182,8 +182,8 @@ protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]>
//noinspection unchecked
validate(result, (T) propValue, propsProvider);
} else {
result.add(propertyName + "' is expected to be of type " + type + ", " +
"but is of type '" + propValue.getClass().getSimpleName() + "'");
result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + ", " +
"but is of type " + propValue.getClass().getSimpleName() + "");
}
}
return result;