minFrom/maxFrom validations against context properties

This commit is contained in:
Michael Hoennig 2024-06-24 16:20:19 +02:00
parent cf6bcc0b94
commit 330ae92c05
16 changed files with 125 additions and 38 deletions

View File

@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -42,6 +43,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import static java.util.Collections.emptyMap;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@ -68,7 +70,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsBookingItemEntity implements Stringifyable, RbacObject {
public class HsBookingItemEntity implements Stringifyable, RbacObject, PropertiesProvider {
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
.withProp(HsBookingItemEntity::getProject)
@ -146,6 +148,23 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
return upperInclusiveFromPostgresDateRange(getValidity());
}
@Override
public Map<String, Object> directProps() {
return resources;
}
@Override
public Object getContextValue(final String propName) {
final var v = resources.get(propName);
if (v!= null) {
return v;
}
if (parentItem!=null) {
return parentItem.getResources().get(propName);
}
return emptyMap();
}
@Override
public String toString() {
return stringify.apply(this);

View File

@ -29,7 +29,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
}
private List<String> validateProperties(final HsBookingItemEntity bookingItem) {
return enrich(prefix(bookingItem.toShortString(), "resources"), validateProperties(bookingItem.getResources()));
return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem));
}
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {

View File

@ -9,6 +9,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -39,6 +40,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import static java.util.Collections.emptyMap;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
@ -63,7 +65,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsHostingAssetEntity implements Stringifyable, RbacObject {
public class HsHostingAssetEntity implements Stringifyable, RbacObject, PropertiesProvider {
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
.withProp(HsHostingAssetEntity::getType)
@ -122,7 +124,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
private PatchableMapWrapper<Object> configWrapper;
@Transient
private boolean isLoaded = false;
private boolean isLoaded;
@PostLoad
public void markAsLoaded() {
@ -137,6 +139,28 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig);
}
@Override
public Map<String, Object> directProps() {
return config;
}
@Override
public Object getContextValue(final String propName) {
final var v = config.get(propName);
if (v!= null) {
return v;
}
if (bookingItem!=null) {
return bookingItem.getResources().get(propName);
}
if (parentAsset!=null && parentAsset.getBookingItem()!=null) {
return parentAsset.getBookingItem().getResources().get(propName);
}
return emptyMap();
}
@Override
public String toString() {
return stringify.apply(this);

View File

@ -76,7 +76,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
}
private List<String> validateProperties(final HsHostingAssetEntity assetEntity) {
return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig()));
return enrich(prefix(assetEntity.toShortString(), "config"), super.validateProperties(assetEntity));
}
private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {

View File

@ -18,14 +18,14 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
AlarmContact.isOptional(),
integerProperty("SSD hard quota").unit("GB").maxFrom("SSD").optional(),
integerProperty("SSD soft quota").unit("GB").minFrom("SSD hard quota").optional(),
integerProperty("SSD soft quota").unit("GB").maxFrom("SSD hard quota").optional(),
integerProperty("HDD hard quota").unit("GB").maxFrom("HDD").optional(),
integerProperty("HDD soft quota").unit("GB").minFrom("HDD hard quota").optional(),
integerProperty("HDD soft quota").unit("GB").maxFrom("HDD hard quota").optional(),
enumerationProperty("shell")
.values("/bin/false", "/bin/bash", "/bin/csh", "/bin/dash", "/usr/bin/tcsh", "/usr/bin/zsh", "/usr/bin/passwd")
.withDefault("/bin/false"),
stringProperty("homedir").readOnly(),
stringProperty("totpKey").matchesRegEx("^0x\\([0-9A-Fa-f][0-9A-Fa-f]\\)+$").minLength(12).maxLength(32).writeOnly().optional(),
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).writeOnly().optional(),
stringProperty("password").minLength(8).maxLength(40).writeOnly()); // FIXME: spec
}

View File

@ -29,9 +29,9 @@ public class BooleanProperty extends ValidatableProperty<Boolean> {
}
@Override
protected void validate(final ArrayList<String> result, final Boolean propValue, final Map<String, Object> props) {
protected void validate(final ArrayList<String> result, final Boolean propValue, final PropertiesProvider propProvider) {
if (falseIf != null && propValue) {
final Object referencedValue = props.get(falseIf.getKey());
final Object referencedValue = propProvider.directProps().get(falseIf.getKey());
if (Objects.equals(referencedValue, falseIf.getValue())) {
result.add(propertyName + "' is expected to be false because " +
falseIf.getKey() + "=" + referencedValue + " but is " + propValue);

View File

@ -5,7 +5,6 @@ import net.hostsharing.hsadminng.mapper.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import static java.util.Arrays.stream;
@ -51,7 +50,7 @@ public class EnumerationProperty extends ValidatableProperty<String> {
}
@Override
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
protected void validate(final ArrayList<String> result, final String propValue, final PropertiesProvider propProvider) {
if (stream(values).noneMatch(v -> v.equals(propValue))) {
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}

View File

@ -38,10 +38,11 @@ public abstract class HsEntityValidator<E> {
.toList();
}
protected ArrayList<String> validateProperties(final Map<String, Object> properties) {
protected ArrayList<String> validateProperties(final PropertiesProvider propsProvider) {
final var result = new ArrayList<String>();
// verify that all actually given properties are specified
final var properties = propsProvider.directProps();
properties.keySet().forEach( givenPropName -> {
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'");
@ -50,7 +51,7 @@ public abstract class HsEntityValidator<E> {
// run all property validators
stream(propertyValidators).forEach(pv -> {
result.addAll(pv.validate(properties));
result.addAll(pv.validate(propsProvider));
});
return result;

View File

@ -2,9 +2,9 @@ package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
import java.util.Map;
@Setter
public class IntegerProperty extends ValidatableProperty<Integer> {
@ -29,6 +29,12 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
super(Integer.class, propertyName, KEY_ORDER);
}
@Override
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given");
Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given");
}
public IntegerProperty minFrom(final String propertyName) {
minFrom = propertyName;
return this;
@ -49,20 +55,34 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
}
@Override
protected void validate(final ArrayList<String> result, final Integer propValue, final Map<String, Object> props) {
if (min != null && propValue < min) {
result.add(propertyName + "' is expected to be >= " + min + " but is " + propValue);
}
if (max != null && propValue > max) {
result.add(propertyName + "' is expected to be <= " + max + " but is " + propValue);
}
protected void validate(final ArrayList<String> result, final Integer propValue, final PropertiesProvider propProvider) {
validateMin(result, propertyName, propValue, min);
validateMax(result, propertyName, propValue, max);
if (step != null && propValue % step != 0) {
result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
}
if (minFrom != null) {
validateMin(result, propertyName, propValue, propProvider.getContextValue(minFrom, Integer.class));
}
if (maxFrom != null) {
validateMax(result, propertyName, propValue, propProvider.getContextValue(maxFrom, Integer.class, 0));
}
}
@Override
protected String simpleTypeName() {
return "integer";
}
private static void validateMin(final ArrayList<String> result, final String propertyName, final Integer propValue, final Integer min) {
if (min != null && propValue < min) {
result.add(propertyName + "' is expected to be at least " + min + " but is " + propValue);
}
}
private static void validateMax(final ArrayList<String> result, final String propertyName, final Integer propValue, final Integer max) {
if (max != null && propValue > max) {
result.add(propertyName + "' is expected to be at most " + max + " but is " + propValue);
}
}
}

View File

@ -4,7 +4,6 @@ import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import java.util.ArrayList;
import java.util.Map;
import java.util.regex.Pattern;
@ -57,7 +56,7 @@ public class StringProperty extends ValidatableProperty<String> {
}
@Override
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
protected void validate(final ArrayList<String> result, final String propValue, final PropertiesProvider propProvider) {
if (minLength != null && propValue.length()<minLength) {
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of '" + propValue+ "' is " + propValue.length());
}
@ -67,6 +66,9 @@ public class StringProperty extends ValidatableProperty<String> {
if (regExPattern != null && !regExPattern.matcher(propValue).matches()) {
result.add(propertyName + "' is expected to be match " + regExPattern + " but '" + propValue+ "' does not match");
}
if (readOnly && propValue != null) {
result.add(propertyName + "' is readonly but given as '" + propValue+ "'");
}
}
@Override

View File

@ -116,8 +116,9 @@ public abstract class ValidatableProperty<T> {
return this;
}
public final List<String> validate(final Map<String, Object> props) {
public final List<String> validate(final PropertiesProvider propsProvider) {
final var result = new ArrayList<String>();
final var props = propsProvider.directProps();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
@ -127,7 +128,7 @@ public abstract class ValidatableProperty<T> {
if (propValue != null){
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, (T) propValue, props);
validate(result, (T) propValue, propsProvider);
} else {
result.add(propertyName + "' is expected to be of type " + type + ", " +
"but is of type '" + propValue.getClass().getSimpleName() + "'");
@ -136,7 +137,7 @@ public abstract class ValidatableProperty<T> {
return result;
}
protected abstract void validate(final ArrayList<String> result, final T propValue, final Map<String, Object> props);
protected abstract void validate(final ArrayList<String> result, final T propValue, final PropertiesProvider propProvider);
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null ) {

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.mapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -43,4 +44,8 @@ public class Array {
.toArray(String[]::new);
return joined;
}
public static <T> T[] emptyArray() {
return of();
}
}

View File

@ -37,7 +37,7 @@ public class TestHsBookingItem {
.type(HsBookingItemType.MANAGED_WEBSPACE)
.caption("test managed webspace item")
.resources(Map.ofEntries(
entry("SSD", 25),
entry("SSD", 50),
entry("Traffic", 250)
))
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))

View File

@ -292,8 +292,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
"statusPhrase": "Bad Request",
"message": "[
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be at most 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be at least 10 but is 0
<<<]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on

View File

@ -37,8 +37,8 @@ class HsManagedServerHostingAssetValidatorUnitTest {
assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
}

View File

@ -36,6 +36,12 @@ class HsUnixUserHostingAssetValidatorUnitTest {
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("abc00-temp")
.caption("some valid test UnixUser")
.config(Map.ofEntries(
entry("SSD hard quota", 50),
entry("SSD soft quota", 40),
entry("totpKey", "0x123456789abcdef01234"),
entry("password", "Hallo Computer, lass mich rein!")
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
@ -55,13 +61,14 @@ class HsUnixUserHostingAssetValidatorUnitTest {
.identifier("abc00-temp")
.caption("some test UnixUser with invalid properties")
.config(Map.ofEntries(
entry("SSD hard quota", 1000),
entry("SSD soft quota", 2000),
entry("HDD hard quota", 1000),
entry("HDD soft quota", 2000),
entry("SSD hard quota", 100),
entry("SSD soft quota", 200),
entry("HDD hard quota", 100),
entry("HDD soft quota", 200),
entry("shell", "/is/invalid"),
entry("homedir", "/is/read-only"),
entry("totpKey", "should be a hex number"),
entry("password", "should be a hex number")
entry("password", "short")
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
@ -70,7 +77,16 @@ class HsUnixUserHostingAssetValidatorUnitTest {
final var result = validator.validate(unixUserHostingAsset);
// then
assertThat(result).isEmpty();
assertThat(result).containsExactlyInAnyOrder(
"'UNIX_USER:abc00-temp.config.SSD hard quota' is expected to be at most 50 but is 100",
"'UNIX_USER:abc00-temp.config.SSD soft quota' is expected to be at most 100 but is 200",
"'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.shell' is expected to be one of [/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd] but is '/is/invalid'",
"'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'",
"'UNIX_USER:abc00-temp.config.totpKey' is expected to be match ^0x([0-9A-Fa-f]{2})+$ but 'should be a hex number' does not match",
"'UNIX_USER:abc00-temp.config.password' length is expected to be at min 8 but length of 'short' is 5"
);
}
@Test