From cf6bcc0b94bd70635fb2790c24d4219b6cd1533c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 24 Jun 2024 15:29:36 +0200 Subject: [PATCH 01/11] add UnixUser HostingAsset property validation config --- .../HsHostingAssetEntityValidator.java | 4 +- .../HsUnixUserHostingAssetValidator.java | 24 ++++-- .../hs/validation/EnumerationProperty.java | 2 +- .../hs/validation/HsEntityValidator.java | 5 ++ .../hs/validation/IntegerProperty.java | 12 +++ .../hs/validation/StringProperty.java | 76 +++++++++++++++++++ .../hs/booking/item/TestHsBookingItem.java | 36 ++++++--- ...UnixUserHostingAssetValidatorUnitTest.java | 65 +++++++++++++++- 8 files changed, 204 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 15ea12df..8afbf99a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -47,7 +47,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validate(final HsHostingAssetEntity assetEntity) { return sequentiallyValidate( - () -> validateEntityReferences(assetEntity), + () -> validateEntityReferencesAndProperties(assetEntity), () -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem () -> optionallyValidate(assetEntity.getBookingItem()), () -> optionallyValidate(assetEntity.getParentAsset()), @@ -55,7 +55,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validateEntityReferences(final HsHostingAssetEntity assetEntity) { + private List validateEntityReferencesAndProperties(final HsHostingAssetEntity assetEntity) { return Stream.of( validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate), validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index dfe222fc..75076cc7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -5,14 +5,28 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import java.util.regex.Pattern; +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { HsUnixUserHostingAssetValidator() { - super(BookingItem.mustBeNull(), - ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), - AssignedToAsset.mustBeNull(), - AlarmContact.isOptional(), // TODO.spec: for quota notifications - NO_EXTRA_PROPERTIES); // TODO.spec: yet to be specified + super( BookingItem.mustBeNull(), + ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), + + integerProperty("SSD hard quota").unit("GB").maxFrom("SSD").optional(), + integerProperty("SSD soft quota").unit("GB").minFrom("SSD hard quota").optional(), + integerProperty("HDD hard quota").unit("GB").maxFrom("HDD").optional(), + integerProperty("HDD soft quota").unit("GB").minFrom("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("password").minLength(8).maxLength(40).writeOnly()); // FIXME: spec } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 923d7ae1..262fc1ac 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -35,7 +35,7 @@ public class EnumerationProperty extends ValidatableProperty { public void deferredInit(final ValidatableProperty[] allProperties) { if (deferredInit != null) { if (this.values != null) { - throw new IllegalStateException("property " + toString() + " already has values"); + throw new IllegalStateException("property " + this + " already has values"); } this.values = deferredInit.apply(allProperties); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 4c20f2a5..7f472583 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -40,14 +40,19 @@ public abstract class HsEntityValidator { protected ArrayList validateProperties(final Map properties) { final var result = new ArrayList(); + + // verify that all actually given properties are specified 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) + "'"); } }); + + // run all property validators stream(propertyValidators).forEach(pv -> { result.addAll(pv.validate(properties)); }); + return result; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index a1658ff9..75d3e79e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -16,7 +16,9 @@ public class IntegerProperty extends ValidatableProperty { private String unit; private Integer min; + private String minFrom; private Integer max; + private String maxFrom; private Integer step; public static IntegerProperty integerProperty(final String propertyName) { @@ -27,6 +29,16 @@ public class IntegerProperty extends ValidatableProperty { super(Integer.class, propertyName, KEY_ORDER); } + public IntegerProperty minFrom(final String propertyName) { + minFrom = propertyName; + return this; + } + + public IntegerProperty maxFrom(final String propertyName) { + maxFrom = propertyName; + return this; + } + @Override public String unit() { return unit; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java new file mode 100644 index 00000000..2755eabb --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -0,0 +1,76 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Pattern; + + +@Setter +public class StringProperty extends ValidatableProperty { + + private static final String[] KEY_ORDER = Array.join( + ValidatableProperty.KEY_ORDER_HEAD, + Array.of("values"), + ValidatableProperty.KEY_ORDER_TAIL); + private Pattern regExPattern; + private Integer minLength; + private Integer maxLength; + private boolean writeOnly; + private boolean readOnly; + + private StringProperty(final String propertyName) { + super(String.class, propertyName, KEY_ORDER); + } + + public static StringProperty stringProperty(final String propertyName) { + return new StringProperty(propertyName); + } + + public StringProperty minLength(final int minLength) { + this.minLength = minLength; + return this; + } + + public StringProperty maxLength(final int maxLength) { + this.maxLength = maxLength; + return this; + } + + public StringProperty matchesRegEx(final String regExPattern) { + this.regExPattern = Pattern.compile(regExPattern); + return this; + } + + public StringProperty writeOnly() { + this.writeOnly = true; + super.optional(); + return this; + } + + public StringProperty readOnly() { + this.readOnly = true; + super.optional(); + return this; + } + + @Override + protected void validate(final ArrayList result, final String propValue, final Map props) { + if (minLength != null && propValue.length()maxLength) { + result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of '" + propValue+ "' is " + propValue.length()); + } + if (regExPattern != null && !regExPattern.matcher(propValue).matches()) { + result.add(propertyName + "' is expected to be match " + regExPattern + " but '" + propValue+ "' does not match"); + } + } + + @Override + protected String simpleTypeName() { + return "string"; + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java index 0779fa2f..039e6fb1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java @@ -12,21 +12,35 @@ import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject. @UtilityClass public class TestHsBookingItem { - public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() - .project(TEST_PROJECT) - .type(HsBookingItemType.MANAGED_SERVER) - .caption("test project booking item") - .resources(Map.ofEntries( - entry("someThing", 1), - entry("anotherThing", "blue") - )) - .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) - .build(); - public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() .project(TEST_PROJECT) .type(HsBookingItemType.CLOUD_SERVER) .caption("test cloud server booking item") .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) .build(); + + public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() + .project(TEST_PROJECT) + .type(HsBookingItemType.MANAGED_SERVER) + .caption("test project booking item") + .resources(Map.ofEntries( + entry("CPUs", 2), + entry("RAM", 4), + entry("SSD", 50), + entry("Traffic", 250) + )) + .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) + .build(); + + public static final HsBookingItemEntity TEST_MANAGED_WEBSPACE_BOOKING_ITEM = HsBookingItemEntity.builder() + .parentItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .type(HsBookingItemType.MANAGED_WEBSPACE) + .caption("test managed webspace item") + .resources(Map.ofEntries( + entry("SSD", 25), + entry("Traffic", 250) + )) + .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) + .build(); + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index afe265b0..3e9b8271 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -1,14 +1,78 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; +import java.util.Map; + +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +import static net.hostsharing.hsadminng.mapper.PatchMap.entry; import static org.assertj.core.api.Assertions.assertThat; class HsUnixUserHostingAssetValidatorUnitTest { + private final HsHostingAssetEntity TEST_MANAGED_SERVER_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_SERVER) + .identifier("vm1234") + .caption("some managed server") + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .build(); + private HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(MANAGED_WEBSPACE) + .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) + .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .identifier("abc00") + .build();; + + @Test + void validatesValidUnixUser() { + // given + final var unixUserHostingAsset = HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("abc00-temp") + .caption("some valid test UnixUser") + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); + + // when + final var result = validator.validate(unixUserHostingAsset); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesUnixUserProperties() { + // given + final var unixUserHostingAsset = HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .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("homedir", "/is/read-only"), + entry("totpKey", "should be a hex number"), + entry("password", "should be a hex number") + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); + + // when + final var result = validator.validate(unixUserHostingAsset); + + // then + assertThat(result).isEmpty(); + } + @Test void validatesInvalidIdentifier() { // given @@ -19,7 +83,6 @@ class HsUnixUserHostingAssetValidatorUnitTest { .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); - // when final var result = validator.validate(unixUserHostingAsset); -- 2.39.5 From 330ae92c05b581ccb0ec5c0164132545939b4d46 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 24 Jun 2024 16:20:19 +0200 Subject: [PATCH 02/11] minFrom/maxFrom validations against context properties --- .../hs/booking/item/HsBookingItemEntity.java | 21 ++++++++++- .../HsBookingItemEntityValidator.java | 2 +- .../hosting/asset/HsHostingAssetEntity.java | 28 +++++++++++++-- .../HsHostingAssetEntityValidator.java | 2 +- .../HsUnixUserHostingAssetValidator.java | 6 ++-- .../hs/validation/BooleanProperty.java | 4 +-- .../hs/validation/EnumerationProperty.java | 3 +- .../hs/validation/HsEntityValidator.java | 5 +-- .../hs/validation/IntegerProperty.java | 36 ++++++++++++++----- .../hs/validation/StringProperty.java | 6 ++-- .../hs/validation/ValidatableProperty.java | 7 ++-- .../hostsharing/hsadminng/mapper/Array.java | 5 +++ .../hs/booking/item/TestHsBookingItem.java | 2 +- ...sHostingAssetControllerAcceptanceTest.java | 4 +-- ...edServerHostingAssetValidatorUnitTest.java | 4 +-- ...UnixUserHostingAssetValidatorUnitTest.java | 28 +++++++++++---- 16 files changed, 125 insertions(+), 38 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 94b80984..ba1d2a7e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -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 stringify = stringify(HsBookingItemEntity.class) .withProp(HsBookingItemEntity::getProject) @@ -146,6 +148,23 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { return upperInclusiveFromPostgresDateRange(getValidity()); } + @Override + public Map 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); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java index ee07e981..315de471 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -29,7 +29,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator 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 optionallyValidate(final HsBookingItemEntity bookingItem) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index ff7bfd33..ae181921 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java @@ -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 stringify = stringify(HsHostingAssetEntity.class) .withProp(HsHostingAssetEntity::getType) @@ -122,7 +124,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { private PatchableMapWrapper 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 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); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 8afbf99a..05bcee97 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -76,7 +76,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator 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 optionallyValidate(final HsHostingAssetEntity assetEntity) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index 75076cc7..d0f0ed27 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -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 } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java index 9d664683..9a1286b0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java @@ -29,9 +29,9 @@ public class BooleanProperty extends ValidatableProperty { } @Override - protected void validate(final ArrayList result, final Boolean propValue, final Map props) { + protected void validate(final ArrayList 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); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 262fc1ac..93ebf242 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -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 { } @Override - protected void validate(final ArrayList result, final String propValue, final Map props) { + protected void validate(final ArrayList 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 + "'"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 7f472583..7dfced5c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -38,10 +38,11 @@ public abstract class HsEntityValidator { .toList(); } - protected ArrayList validateProperties(final Map properties) { + protected ArrayList validateProperties(final PropertiesProvider propsProvider) { final var result = new ArrayList(); // 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 { // run all property validators stream(propertyValidators).forEach(pv -> { - result.addAll(pv.validate(properties)); + result.addAll(pv.validate(propsProvider)); }); return result; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index 75d3e79e..14f97193 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -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 { @@ -29,6 +29,12 @@ public class IntegerProperty extends ValidatableProperty { 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 { } @Override - protected void validate(final ArrayList result, final Integer propValue, final Map 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 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 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 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); + } + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index 2755eabb..ba837287 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -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 { } @Override - protected void validate(final ArrayList result, final String propValue, final Map props) { + protected void validate(final ArrayList result, final String propValue, final PropertiesProvider propProvider) { if (minLength != null && propValue.length() { 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 diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 3b0bb099..021fc90b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -116,8 +116,9 @@ public abstract class ValidatableProperty { return this; } - public final List validate(final Map props) { + public final List validate(final PropertiesProvider propsProvider) { final var result = new ArrayList(); + final var props = propsProvider.directProps(); final var propValue = props.get(propertyName); if (propValue == null) { if (required) { @@ -127,7 +128,7 @@ public abstract class ValidatableProperty { 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 { return result; } - protected abstract void validate(final ArrayList result, final T propValue, final Map props); + protected abstract void validate(final ArrayList result, final T propValue, final PropertiesProvider propProvider); public void verifyConsistency(final Map.Entry, ?> typeDef) { if (required == null ) { diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java index 39588f11..86a4766a 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -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[] emptyArray() { + return of(); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java index 039e6fb1..b2b43df9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java @@ -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))) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 0b231bbd..391b4d3e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -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 diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index 010bbf54..2eb7f581 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -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'"); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 3e9b8271..49019beb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -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 -- 2.39.5 From 3566cb61b6c114677dbce270043dd2b06408b5b5 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 25 Jun 2024 17:12:48 +0200 Subject: [PATCH 03/11] hacked version for treating writeOnly properties --- .../asset/HsHostingAssetController.java | 12 +- ...HsHostingAssetEntityValidatorRegistry.java | 8 +- .../HsUnixUserHostingAssetValidator.java | 5 +- .../hs/validation/BooleanProperty.java | 4 +- .../hs/validation/EnumerationProperty.java | 4 +- .../hs/validation/IntegerProperty.java | 8 +- .../hs/validation/PasswordProperty.java | 63 ++++++ .../hs/validation/PropertiesProvider.java | 31 +++ .../hs/validation/StringProperty.java | 24 ++- .../hs/validation/ValidatableProperty.java | 2 +- .../hsadminng/mapper/PatchableMapWrapper.java | 15 +- ...sHostingAssetControllerAcceptanceTest.java | 192 +++++++++++++----- ...UnixUserHostingAssetValidatorUnitTest.java | 5 +- .../validation/PasswordPropertyUnitTest.java | 92 +++++++++ 14 files changed, 392 insertions(+), 73 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index b7982328..1e386e62 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -71,7 +71,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = validated(assetRepo.save(entityToSave)); + final var saved = saveAndValidate(entityToSave); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -126,7 +126,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, current).apply(body); - final var saved = validated(assetRepo.save(current)); + final var saved = saveAndValidate(current); final var mapped = mapper.map(saved, HsHostingAssetResource.class); return ResponseEntity.ok(mapped); } @@ -144,4 +144,12 @@ public class HsHostingAssetController implements HsHostingAssetsApi { resource.getParentAssetUuid())))); } }; + + HsHostingAssetEntity saveAndValidate(final HsHostingAssetEntity entity) { + final var saved = assetRepo.save(entity); + // FIXME: this is hacky, better remove the properties from the mapped resource object + em.flush(); + em.detach(saved); // validated(...) is going to remove writeOnly properties + return validated(saved); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index 1b9a5241..19de3491 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -40,7 +40,13 @@ public class HsHostingAssetEntityValidatorRegistry { } public static List doValidate(final HsHostingAssetEntity hostingAsset) { - return HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()).validate(hostingAsset); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()); + final var validated = validator.validate(hostingAsset); + + //validator.cleanup() + hostingAsset.getConfig().remove("password"); // FIXME + hostingAsset.getConfig().remove("totpKey"); // FIXME + return validated; } public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index d0f0ed27..e058a734 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -7,6 +7,7 @@ import java.util.regex.Pattern; import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; +import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { @@ -25,8 +26,8 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { .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]{2})+$").minLength(20).maxLength(256).writeOnly().optional(), - stringProperty("password").minLength(8).maxLength(40).writeOnly()); // FIXME: spec + stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).hidden().writeOnly().optional(), + passwordProperty("password").minLength(8).maxLength(40).writeOnly()); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java index 9a1286b0..5f893d74 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java @@ -4,7 +4,7 @@ import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; import java.util.AbstractMap; -import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -29,7 +29,7 @@ public class BooleanProperty extends ValidatableProperty { } @Override - protected void validate(final ArrayList result, final Boolean propValue, final PropertiesProvider propProvider) { + protected void validate(final List result, final Boolean propValue, final PropertiesProvider propProvider) { if (falseIf != null && propValue) { final Object referencedValue = propProvider.directProps().get(falseIf.getKey()); if (Objects.equals(referencedValue, falseIf.getValue())) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 93ebf242..0266e003 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -3,8 +3,8 @@ package net.hostsharing.hsadminng.hs.validation; import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; -import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import static java.util.Arrays.stream; @@ -50,7 +50,7 @@ public class EnumerationProperty extends ValidatableProperty { } @Override - protected void validate(final ArrayList result, final String propValue, final PropertiesProvider propProvider) { + protected void validate(final List 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 + "'"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index 14f97193..37a011d4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -4,7 +4,7 @@ import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; import org.apache.commons.lang3.Validate; -import java.util.ArrayList; +import java.util.List; @Setter public class IntegerProperty extends ValidatableProperty { @@ -55,7 +55,7 @@ public class IntegerProperty extends ValidatableProperty { } @Override - protected void validate(final ArrayList result, final Integer propValue, final PropertiesProvider propProvider) { + protected void validate(final List result, final Integer propValue, final PropertiesProvider propProvider) { validateMin(result, propertyName, propValue, min); validateMax(result, propertyName, propValue, max); if (step != null && propValue % step != 0) { @@ -74,13 +74,13 @@ public class IntegerProperty extends ValidatableProperty { return "integer"; } - private static void validateMin(final ArrayList result, final String propertyName, final Integer propValue, final Integer min) { + private static void validateMin(final List 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 result, final String propertyName, final Integer propValue, final Integer max) { + private static void validateMax(final List 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); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java new file mode 100644 index 00000000..5de5108f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -0,0 +1,63 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; + +import java.util.List; +import java.util.stream.Stream; + +@Setter +public class PasswordProperty extends StringProperty { + + private PasswordProperty(final String propertyName) { + super(propertyName); + hidden(); + } + + public static PasswordProperty passwordProperty(final String propertyName) { + return new PasswordProperty(propertyName); + } + + @Override + protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { + super.validate(result, propValue, propProvider); + validatePassword(result, propValue); + } + + @Override + protected String simpleTypeName() { + return "password"; + } + + private void validatePassword(final List result, final String password) { + boolean hasLowerCase = false; + boolean hasUpperCase = false; + boolean hasDigit = false; + boolean hasSpecialChar = false; + boolean containsColon = false; + + for (char c : password.toCharArray()) { + if (Character.isLowerCase(c)) { + hasLowerCase = true; + } else if (Character.isUpperCase(c)) { + hasUpperCase = true; + } else if (Character.isDigit(c)) { + hasDigit = true; + } else if (!Character.isLetterOrDigit(c)) { + hasSpecialChar = true; + } + + if (c == ':') { + containsColon = true; + } + } + + final long groupsCovered = Stream.of(hasLowerCase, hasUpperCase, hasDigit, hasSpecialChar).filter(v->v).count(); + if ( groupsCovered < 3) { + result.add(propertyName + "' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters"); + } + if (containsColon) { + result.add(propertyName + "' must not contain colon (':')"); + } + + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java new file mode 100644 index 00000000..c4d60fb8 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.hs.validation; + +import java.util.Map; + +public interface PropertiesProvider { + + Map directProps(); + Object getContextValue(final String propName); + + default T getDirectValue(final String propName, final Class clazz) { + return cast(propName, directProps().get(propName), clazz, null); + } + + default T getContextValue(final String propName, final Class clazz) { + return cast(propName, getContextValue(propName), clazz, null); + } + + default T getContextValue(final String propName, final Class clazz, final T defaultValue) { + return cast(propName, getContextValue(propName), clazz, defaultValue); + } + + private static T cast( final String propName, final Object value, final Class clazz, final T defaultValue) { + if (value == null && defaultValue != null) { + return defaultValue; + } + if (value == null || clazz.isInstance(value)) { + return clazz.cast(value); + } + throw new IllegalStateException(propName + " expected to be an "+clazz.getSimpleName()+", but got '" + value + "'"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index ba837287..ab392383 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.validation; import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; -import java.util.ArrayList; +import java.util.List; import java.util.regex.Pattern; @@ -19,8 +19,9 @@ public class StringProperty extends ValidatableProperty { private Integer maxLength; private boolean writeOnly; private boolean readOnly; + private boolean hidden; - private StringProperty(final String propertyName) { + protected StringProperty(final String propertyName) { super(String.class, propertyName, KEY_ORDER); } @@ -43,6 +44,11 @@ public class StringProperty extends ValidatableProperty { return this; } + public StringProperty hidden() { + this.hidden = true; + return this; + } + public StringProperty writeOnly() { this.writeOnly = true; super.optional(); @@ -56,21 +62,25 @@ public class StringProperty extends ValidatableProperty { } @Override - protected void validate(final ArrayList result, final String propValue, final PropertiesProvider propProvider) { + protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { if (minLength != null && propValue.length()maxLength) { - result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of '" + propValue+ "' is " + propValue.length()); + result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length()); } if (regExPattern != null && !regExPattern.matcher(propValue).matches()) { - result.add(propertyName + "' is expected to be match " + regExPattern + " but '" + propValue+ "' does not match"); + result.add(propertyName + "' is expected to be match " + regExPattern + " but " + display(propValue) + " does not match"); } if (readOnly && propValue != null) { - result.add(propertyName + "' is readonly but given as '" + propValue+ "'"); + result.add(propertyName + "' is readonly but given as " + display(propValue)); } } + private String display(final String propValue) { + return hidden ? "provided value" : ("'" + propValue + "'"); + } + @Override protected String simpleTypeName() { return "string"; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 021fc90b..5bc85e29 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -137,7 +137,7 @@ public abstract class ValidatableProperty { return result; } - protected abstract void validate(final ArrayList result, final T propValue, final PropertiesProvider propProvider); + protected abstract void validate(final List result, final T propValue, final PropertiesProvider propProvider); public void verifyConsistency(final Map.Entry, ?> typeDef) { if (required == null ) { diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java index 4962ac8d..21153b14 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java @@ -53,13 +53,20 @@ public class PatchableMapWrapper implements Map { } public String toString() { - return "{ " + return "{\n" + ( keySet().stream().sorted() - .map(k -> k + ": " + get(k))) - .collect(joining(", ") + .map(k -> " \"" + k + "\": " + optionallyQuoted(get(k)))) + .collect(joining(",\n") ) - + " }"; + + "\n}\n"; + } + + private Object optionallyQuoted(final Object value) { + if ( value instanceof Number || value instanceof Boolean ) { + return value; + } + return "\"" + value + "\""; } // --- below just delegating methods -------------------------------- diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 391b4d3e..fb1f8512 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -25,12 +25,14 @@ import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.function.Supplier; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.strictlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; @@ -73,7 +75,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup // given context("superuser-alex@hostsharing.net"); final var givenProject = projectRepo.findByCaption("D-1000111 default project").stream() - .findAny().orElseThrow(); + .findAny().orElseThrow(); RestAssured // @formatter:off .given() @@ -264,7 +266,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void propertyValidationsArePerformend_whenAddingAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project", + final var givenBookingItem = givenSomeNewBookingItem( + "D-1000111 default project", HsBookingItemType.MANAGED_SERVER, "some PrivateCloud"); @@ -299,7 +302,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup """.replaceAll(" +<<<", ""))); // @formatter:on } - @Test void totalsLimitValidationsArePerformend_whenAddingAsset() { @@ -311,7 +313,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - for (int n = 0; n < 25; ++n ) { + for (int n = 0; n < 25; ++n) { toCleanup(assetRepo.save( HsHostingAssetEntity.builder() .type(UNIX_USER) @@ -358,8 +360,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canGetArbitraryAsset() { context.define("superuser-alex@hostsharing.net"); final var givenAssetUuid = assetRepo.findByIdentifier("vm1011").stream() - .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project")) - .findAny().orElseThrow().getUuid(); + .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project")) + .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off .given() @@ -429,8 +431,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() { - final var givenAsset = givenSomeTemporaryHostingAsset("2001", MANAGED_SERVER, - config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .bookingItem(givenSomeNewBookingItem( + "D-1000111 default project", + HsBookingItemType.MANAGED_SERVER, + "temp ManagedServer")) + .type(MANAGED_SERVER) + .identifier("vm2001") + .caption("some test-asset") + .config(Map.ofEntries( + Map.entry("monit_max_ssd_usage", 80), + Map.entry("monit_max_hdd_usage", 90), + Map.entry("monit_max_cpu_usage", 90), + Map.entry("monit_max_ram_usage", 70) + )) + .build()); final var alarmContactUuid = givenContact().getUuid(); RestAssured // @formatter:off @@ -474,6 +491,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup // @formatter:on // finally, the asset is actually updated + em.clear(); context.define("superuser-alex@hostsharing.net"); assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get() .matches(asset -> { @@ -484,13 +502,76 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup return true; }); } - } - private HsOfficeContactEntity givenContact() { - return jpaAttempt.transacted(() -> { - context.define("superuser-alex@hostsharing.net"); - return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow(); - }).returnedValue(); + @Test + void assetAdmin_canPatchAllUpdatablePropertiesOfAsset() { + + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .type(UNIX_USER) + .parentAsset(givenHostingAsset(MANAGED_WEBSPACE, "fir01")) + .identifier("fir01-temp") + .caption("some test-unix-user") + .build()); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + //.header("assumed-roles", "hs_hosting_asset#vm2001:ADMIN") + .contentType(ContentType.JSON) + .body(""" + { + "caption": "some patched test-unix-user", + "config": { + "shell": "/bin/bash", + "totpKey": "0x1234567890abcdef0123456789abcdef", + "password": "Ein Passwort mit 4 Zeichengruppen!" + } + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/hosting/assets/" + givenAsset.getUuid()) + .then().log().all().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "UNIX_USER", + "identifier": "fir01-temp", + "caption": "some patched test-unix-user", + "config": { + "shell": "/bin/bash" + } + } + """)) + // the config separately but not-leniently to make sure that no write-only-properties are listed + .body("config", strictlyEquals(""" + { + "shell": "/bin/bash" + } + """)) + ; + // @formatter:on + + // finally, the asset is actually updated + assertThat(jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + return assetRepo.findByUuid(givenAsset.getUuid()); + }).returnedValue()).isPresent().get() + .matches(asset -> { + assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user"); + assertThat(asset.getConfig().toString()).isEqualTo(""" + { + "password": "Ein Passwort mit 4 Zeichengruppen!", + "shell": "/bin/bash", + "totpKey": "0x1234567890abcdef0123456789abcdef" + } + """); + return true; + }); + } } @Nested @@ -500,9 +581,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canDeleteArbitraryAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAsset = givenSomeTemporaryHostingAsset("1002", MANAGED_SERVER, - config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); - + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .bookingItem(givenSomeNewBookingItem( + "D-1000111 default project", + HsBookingItemType.MANAGED_SERVER, + "temp ManagedServer")) + .type(MANAGED_SERVER) + .identifier("vm1002") + .caption("some test-asset") + .config(Map.ofEntries( + Map.entry("monit_max_ssd_usage", 80), + Map.entry("monit_max_hdd_usage", 90), + Map.entry("monit_max_cpu_usage", 90), + Map.entry("monit_max_ram_usage", 70) + )) + .build()); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") @@ -519,9 +614,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void normalUser_canNotDeleteUnrelatedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAsset = givenSomeTemporaryHostingAsset("1003", MANAGED_SERVER, - config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); - + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .bookingItem(givenSomeNewBookingItem( + "D-1000111 default project", + HsBookingItemType.MANAGED_SERVER, + "temp ManagedServer")) + .type(MANAGED_SERVER) + .identifier("vm1003") + .caption("some test-asset") + .config(Map.ofEntries( + Map.entry("monit_max_ssd_usage", 80), + Map.entry("monit_max_hdd_usage", 90), + Map.entry("monit_max_cpu_usage", 90), + Map.entry("monit_max_ram_usage", 70) + )) + .build()); RestAssured // @formatter:off .given() .header("current-user", "selfregistered-user-drew@hostsharing.org") @@ -538,7 +647,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) { return assetRepo.findByIdentifier(identifier).stream() - .filter(ha -> ha.getType()==type) + .filter(ha -> ha.getType() == type) .findAny().orElseThrow(); } @@ -559,12 +668,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup }).assertSuccessful().returnedValue(); } - HsBookingItemEntity givenSomeNewBookingItem(final String projectCaption, final HsBookingItemType bookingItemType, final String bookingItemCaption) { + HsBookingItemEntity givenSomeNewBookingItem( + final String projectCaption, + final HsBookingItemType bookingItemType, + final String bookingItemCaption) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); final var project = projectRepo.findByCaption(projectCaption).getFirst(); final var resources = switch (bookingItemType) { - case MANAGED_SERVER -> Map.ofEntries(entry("CPUs", 1), entry("RAM", 20), entry("SSD", 25), entry("Traffic", 250)); + case MANAGED_SERVER -> Map.ofEntries(entry("CPUs", 1), + entry("RAM", 20), + entry("SSD", 25), + entry("Traffic", 250)); default -> new HashMap(); }; final var newBookingItem = HsBookingItemEntity.builder() @@ -584,33 +699,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup return givenAsset; } - @SafeVarargs - private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix, - final HsHostingAssetType hostingAssetType, - final Map.Entry... config) { + private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final Supplier newAsset) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - final var bookingItemType = switch (hostingAssetType) { - case CLOUD_SERVER -> HsBookingItemType.CLOUD_SERVER; - case MANAGED_SERVER -> HsBookingItemType.MANAGED_SERVER; - case MANAGED_WEBSPACE -> HsBookingItemType.MANAGED_WEBSPACE; - default -> null; - }; - final var newBookingItem = givenSomeNewBookingItem("D-1000111 default project", bookingItemType, "temp ManagedServer"); - final var newAsset = HsHostingAssetEntity.builder() - .uuid(UUID.randomUUID()) - .bookingItem(newBookingItem) - .type(hostingAssetType) - .identifier("vm" + identifierSuffix) - .caption("some test-asset") - .config(Map.ofEntries(config)) - .build(); - - return assetRepo.save(newAsset); + return toCleanup(assetRepo.save(newAsset.get())); }).assertSuccessful().returnedValue(); } - private Map.Entry config(final String key, final Object value) { - return entry(key, value); + private HsOfficeContactEntity givenContact() { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow(); + }).returnedValue(); } + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 49019beb..15f2de6c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -84,8 +84,9 @@ class HsUnixUserHostingAssetValidatorUnitTest { "'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" + "'UNIX_USER:abc00-temp.config.totpKey' is expected to be 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" ); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java new file mode 100644 index 00000000..66da5f2d --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java @@ -0,0 +1,92 @@ +package net.hostsharing.hsadminng.hs.validation; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; +import static org.assertj.core.api.Assertions.assertThat; + +class PasswordPropertyUnitTest { + + private final ValidatableProperty passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly(); + private final List violations = new ArrayList<>(); + + @ParameterizedTest + @ValueSource(strings = { + "lowerUpperAndDigit1", + "lowerUpperAndSpecial!", + "digit1LowerAndSpecial!", + "digit1special!lower", + "DIGIT1SPECIAL!UPPER" }) + void shouldValidateValidPassword(final String password) { + // when + passwordProp.validate(violations, password, null); + + // then + assertThat(violations).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { + "noDigitNoSpecial", + "!!!!!!12345", + "nolower-nodigit", + "nolower1nospecial", + "NOLOWER-NODIGIT", + "NOLOWER1NOSPECIAL" + }) + void shouldRecognizeMissingCharacterGroup(final String givenPassword) { + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations) + .contains("password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters") + .doesNotContain(givenPassword); + } + + @Test + void shouldRecognizeTooShortPassword() { + // given + final String givenPassword = "0123456"; + + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations) + .contains("password' length is expected to be at min 8 but length of provided value is 7") + .doesNotContain(givenPassword); + } + + @Test + void shouldRecognizeTooLongPassowrd() { + // given + final String givenPassword = "password' length is expected to be at max 40 but is 41"; + + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations).contains("password' length is expected to be at max 40 but length of provided value is 54") + .doesNotContain(givenPassword); + } + + @Test + void shouldRecognizeColonInPassword() { + // given + final String givenPassword = "lowerUpper:1234"; + + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations) + .contains("password' must not contain colon (':')") + .doesNotContain(givenPassword); + } +} -- 2.39.5 From 680b67f162a3793b581d7d317eeb8f302fe14184 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 25 Jun 2024 17:37:52 +0200 Subject: [PATCH 04/11] still hacked, but now cleaning up the resource object, not the entity --- .../asset/HsHostingAssetController.java | 17 +++++++---------- .../HsHostingAssetEntityValidatorRegistry.java | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 1e386e62..179a2d37 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.cleanup; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated; @RestController @@ -71,7 +72,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = saveAndValidate(entityToSave); + final var saved = validated(assetRepo.save(entityToSave)); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -126,8 +127,8 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, current).apply(body); - final var saved = saveAndValidate(current); - final var mapped = mapper.map(saved, HsHostingAssetResource.class); + final var saved = validated(assetRepo.save(current)); + final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } @@ -145,11 +146,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { } }; - HsHostingAssetEntity saveAndValidate(final HsHostingAssetEntity entity) { - final var saved = assetRepo.save(entity); - // FIXME: this is hacky, better remove the properties from the mapped resource object - em.flush(); - em.detach(saved); // validated(...) is going to remove writeOnly properties - return validated(saved); - } + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { + cleanup(entity, resource); + }; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index 19de3491..c153784c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; import net.hostsharing.hsadminng.errors.MultiValidationException; @@ -41,12 +42,7 @@ public class HsHostingAssetEntityValidatorRegistry { public static List doValidate(final HsHostingAssetEntity hostingAsset) { final var validator = HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()); - final var validated = validator.validate(hostingAsset); - - //validator.cleanup() - hostingAsset.getConfig().remove("password"); // FIXME - hostingAsset.getConfig().remove("totpKey"); // FIXME - return validated; + return validator.validate(hostingAsset); } public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) { @@ -54,4 +50,14 @@ public class HsHostingAssetEntityValidatorRegistry { return entityToSave; } + public static void cleanup(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) { + final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); +// final var validated = validator.cleanup(hostingAsset); + //validator.cleanup() + final var config = new HashMap<>((Map) resource.getConfig()); + config.remove("password"); // FIXME + config.remove("totpKey"); // FIXME + resource.setConfig(config); + } + } -- 2.39.5 From 6318497294389e9c647091144ed6ac88f89e7185 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 25 Jun 2024 17:50:54 +0200 Subject: [PATCH 05/11] still a bit hacky, but now working generically --- .../HsHostingAssetEntityValidatorRegistry.java | 14 +++++++++----- .../hs/validation/HsEntityValidator.java | 7 +++++++ .../hsadminng/hs/validation/StringProperty.java | 14 -------------- .../hs/validation/ValidatableProperty.java | 17 +++++++++++++++++ 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index c153784c..ef0a4899 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -52,12 +52,16 @@ public class HsHostingAssetEntityValidatorRegistry { public static void cleanup(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) { final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); -// final var validated = validator.cleanup(hostingAsset); - //validator.cleanup() - final var config = new HashMap<>((Map) resource.getConfig()); - config.remove("password"); // FIXME - config.remove("totpKey"); // FIXME + final var config = validator.cleanup(asMap(resource)); resource.setConfig(config); } + @SuppressWarnings("unchecked") + private static Map asMap(final HsHostingAssetResource resource) { + if (resource.getConfig() instanceof Map map) { + return map; + } + throw new IllegalArgumentException("expected a Map, but got a " + resource.getConfig().getClass()); + } + } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 7dfced5c..67540554 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.validation; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -86,4 +87,10 @@ public abstract class HsEntityValidator { } throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } + + public Map cleanup(final Map config) { + final var copy = new HashMap<>(config); + stream(propertyValidators).filter(p -> p.writeOnly).forEach(p -> copy.remove(p.propertyName)); + return copy; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index ab392383..805f3d0b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -17,8 +17,6 @@ public class StringProperty extends ValidatableProperty { private Pattern regExPattern; private Integer minLength; private Integer maxLength; - private boolean writeOnly; - private boolean readOnly; private boolean hidden; protected StringProperty(final String propertyName) { @@ -49,18 +47,6 @@ public class StringProperty extends ValidatableProperty { return this; } - public StringProperty writeOnly() { - this.writeOnly = true; - super.optional(); - return this; - } - - public StringProperty readOnly() { - this.readOnly = true; - super.optional(); - return this; - } - @Override protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { if (minLength != null && propValue.length() { private final String[] keyOrder; private Boolean required; private T defaultValue; + protected boolean readOnly; + protected boolean writeOnly; + protected Function[], T[]> deferredInit; private boolean isTotalsValidator = false; @JsonIgnore @@ -43,6 +46,20 @@ public abstract class ValidatableProperty { return null; } + + public ValidatableProperty writeOnly() { + this.writeOnly = true; + optional(); + return this; + } + + public ValidatableProperty readOnly() { + this.readOnly = true; + optional(); + return this; + } + + public ValidatableProperty required() { required = TRUE; return this; -- 2.39.5 From c2cd6c2f23599e1808345655d7628676be58620e Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 26 Jun 2024 15:11:21 +0200 Subject: [PATCH 06/11] implement ValidatableProperty.computedBy and fix test data --- .../asset/HsHostingAssetController.java | 18 ++++++++----- ...HsHostingAssetEntityValidatorRegistry.java | 4 +-- .../HsUnixUserHostingAssetValidator.java | 12 ++++++++- .../hs/validation/HsEntityValidator.java | 14 ++++++++--- .../hs/validation/PasswordProperty.java | 2 ++ .../hs/validation/ValidatableProperty.java | 14 +++++++++++ .../item/HsBookingItemEntityUnitTest.java | 2 +- ...sBookingItemRepositoryIntegrationTest.java | 20 +++++++++------ ...sHostingAssetControllerAcceptanceTest.java | 25 +++++++++++++------ .../asset/HsHostingAssetEntityUnitTest.java | 12 ++++----- ...HostingAssetRepositoryIntegrationTest.java | 4 ++- .../hsadminng/rbac/test/JsonMatcher.java | 14 +++++++---- 12 files changed, 101 insertions(+), 40 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 179a2d37..163aeae1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.context.Context; @@ -23,7 +24,6 @@ import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.cleanup; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated; @RestController @@ -79,7 +79,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { .path("/api/hs/hosting/assets/{id}") .buildAndExpand(saved.getUuid()) .toUri(); - final var mapped = mapper.map(saved, HsHostingAssetResource.class); + final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.created(uri).body(mapped); } @@ -95,7 +95,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var result = assetRepo.findByUuid(assetUuid); return result .map(assetEntity -> ResponseEntity.ok( - mapper.map(assetEntity, HsHostingAssetResource.class))) + mapper.map(assetEntity, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER))) .orElseGet(() -> ResponseEntity.notFound().build()); } @@ -127,6 +127,13 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, current).apply(body); +// validate(current)/ / self-validation, hashing passwords etc. +// .then(HsHostingAssetEntityValidatorRegistry::prepareForSave) // hashing passwords etc. +// .then(assetRepo::save) +// .then(HsHostingAssetEntityValidatorRegistry::validateInContext) +// .then(this::mapToResource) using postprocessProperties to remove write-only + add read-only properties + + final var saved = validated(assetRepo.save(current)); final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); @@ -146,7 +153,6 @@ public class HsHostingAssetController implements HsHostingAssetsApi { } }; - final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { - cleanup(entity, resource); - }; + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER + = HsHostingAssetEntityValidatorRegistry::postprocessProperties; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index ef0a4899..df00c710 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -50,9 +50,9 @@ public class HsHostingAssetEntityValidatorRegistry { return entityToSave; } - public static void cleanup(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) { + public static void postprocessProperties(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) { final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); - final var config = validator.cleanup(asMap(resource)); + final var config = validator.postprocess(entity, asMap(resource)); resource.setConfig(config); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index e058a734..6b390767 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import java.util.regex.Pattern; @@ -12,6 +13,8 @@ import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringPrope class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { + private static final int DASH_LENGTH = "-".length(); + HsUnixUserHostingAssetValidator() { super( BookingItem.mustBeNull(), ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), @@ -25,7 +28,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { 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("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).hidden().writeOnly().optional(), passwordProperty("password").minLength(8).maxLength(40).writeOnly()); } @@ -35,4 +38,11 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$"); } + + private static String computeHomedir(final PropertiesProvider propertiesProvider) { + final var entity = (HsHostingAssetEntity) propertiesProvider; + final var webspaceName = entity.getParentAsset().getIdentifier(); + return "/home/pacs/" + webspaceName + + "/users/" + entity.getIdentifier().substring(webspaceName.length()+DASH_LENGTH); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 67540554..9bc4b3cb 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -11,7 +11,8 @@ import java.util.function.Supplier; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; -public abstract class HsEntityValidator { +// TODO.refa: rename to HsEntityProcessor, also subclasses +public abstract class HsEntityValidator { public final ValidatableProperty[] propertyValidators; @@ -88,9 +89,16 @@ public abstract class HsEntityValidator { throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } - public Map cleanup(final Map config) { + public Map postprocess(final E entity, final Map config) { final var copy = new HashMap<>(config); - stream(propertyValidators).filter(p -> p.writeOnly).forEach(p -> copy.remove(p.propertyName)); + stream(propertyValidators).forEach(p -> { + if ( p.writeOnly) { + copy.remove(p.propertyName); + } + if (p.isComputed()) { + copy.put(p.propertyName, p.compute(entity)); + } + }); return copy; } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index 5de5108f..441f0d06 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -23,6 +23,8 @@ public class PasswordProperty extends StringProperty { validatePassword(result, propValue); } + // TODO.impl: only a SHA512 hash should be stored in the database, not the password itself + @Override protected String simpleTypeName() { return "password"; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 9bdf064b..c65f9325 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -32,6 +32,7 @@ public abstract class ValidatableProperty { private final String[] keyOrder; private Boolean required; private T defaultValue; + protected Function computedBy; protected boolean readOnly; protected boolean writeOnly; @@ -216,4 +217,17 @@ public abstract class ValidatableProperty { .flatMap(Collection::stream) .toList(); } + + public ValidatableProperty computedBy(final Function compute) { + this.computedBy = compute; + return this; + } + + public boolean isComputed() { + return computedBy != null; + } + + public T compute(final E entity) { + return computedBy.apply(entity); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java index 903d5385..258b55b7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java @@ -53,7 +53,7 @@ class HsBookingItemEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); - assertThat(result).isEqualTo("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index f125974a..5e32e23d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -170,9 +170,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", - "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", - "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )", + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )", + "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )"); assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny()) .as("at least one relatedProject expected, but none found => fetching relatedProject does not work") .isNotEmpty(); @@ -193,9 +193,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", - "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", - "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )", + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )", + "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )"); } } @@ -348,13 +348,17 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup final List actualResult, final String... bookingItemNames) { assertThat(actualResult) - .extracting(bookingItemEntity -> bookingItemEntity.toString()) + .extracting(HsBookingItemEntity::toString) + .extracting(string-> string.replaceAll("\\s+", " ")) + .extracting(string-> string.replaceAll("\"", "")) .containsExactlyInAnyOrder(bookingItemNames); } void allTheseBookingItemsAreReturned(final List actualResult, final String... bookingItemNames) { assertThat(actualResult) - .extracting(bookingItemEntity -> bookingItemEntity.toString()) + .extracting(HsBookingItemEntity::toString) + .extracting(string -> string.replaceAll("\\s+", " ")) + .extracting(string -> string.replaceAll("\"", "")) .contains(bookingItemNames); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index fb1f8512..11bfc45c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -476,9 +476,10 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "vm2001", "caption": "some test-asset", "alarmContact": { - "uuid": "%s", "caption": "second contact", - "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } + "emailAddresses": { + "main": "contact-admin@secondcontact.example.com" + } }, "config": { "monit_max_cpu_usage": 90, @@ -487,7 +488,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "monit_min_free_ssd": 5 } } - """.formatted(alarmContactUuid))); + """)); // @formatter:on // finally, the asset is actually updated @@ -495,10 +496,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get() .matches(asset -> { - assertThat(asset.getAlarmContact().toString()).isEqualTo( - "contact(caption='second contact', emailAddresses='{ main: contact-admin@secondcontact.example.com }')"); - assertThat(asset.getConfig().toString()).isEqualTo( - "{ monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 }"); + assertThat(asset.getAlarmContact()).isNotNull() + .extracting(c -> c.getEmailAddresses().get("main")) + .isEqualTo("contact-admin@secondcontact.example.com"); + assertThat(asset.getConfig().toString()) + .isEqualToIgnoringWhitespace(""" + { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 70, + "monit_max_ssd_usage": 85, + "monit_min_free_ssd": 5 + } + """); return true; }); } @@ -542,6 +551,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "fir01-temp", "caption": "some patched test-unix-user", "config": { + "homedir": "/home/pacs/fir01/users/temp", "shell": "/bin/bash" } } @@ -549,6 +559,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup // the config separately but not-leniently to make sure that no write-only-properties are listed .body("config", strictlyEquals(""" { + "homedir": "/home/pacs/fir01/users/temp", "shell": "/bin/bash" } """)) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java index 1dd7c0e1..6460ae39 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java @@ -57,14 +57,14 @@ class HsHostingAssetEntityUnitTest { @Test void toStringContainsAllPropertiesAndResourcesSortedByKey() { - assertThat(givenWebspace.toString()).isEqualTo( - "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + assertThat(givenWebspace.toString()).isEqualToIgnoringWhitespace( + "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })"); - assertThat(givenUnixUser.toString()).isEqualTo( - "HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { HDD-hard-quota: 512, HDD-soft-quota: 256, SSD-hard-quota: 256, SSD-soft-quota: 128 })"); + assertThat(givenUnixUser.toString()).isEqualToIgnoringWhitespace( + "HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { \"HDD-hard-quota\": 512, \"HDD-soft-quota\": 256, \"SSD-hard-quota\": 256, \"SSD-soft-quota\": 128 })"); - assertThat(givenDomainHttpSetup.toString()).isEqualTo( - "HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { option-htdocsfallback: true, use-fcgiphpbin: /usr/lib/cgi-bin/php, validsubdomainnames: * })"); + assertThat(givenDomainHttpSetup.toString()).isEqualToIgnoringWhitespace( + "HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { \"option-htdocsfallback\": true, \"use-fcgiphpbin\": \"/usr/lib/cgi-bin/php\", \"validsubdomainnames\": \"*\" })"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java index 5ada81b0..6c79da67 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java @@ -195,7 +195,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu exactlyTheseAssetsAreReturned( result, "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)", - "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 })"); + "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 } )"); } @Test @@ -407,6 +407,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu final String... serverNames) { assertThat(actualResult) .extracting(HsHostingAssetEntity::toString) + .extracting(input -> input.replaceAll("\\s+", " ")) + .extracting(input -> input.replaceAll("\"", "")) .containsExactlyInAnyOrder(serverNames); } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java index 54208e4c..9431c184 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java @@ -9,13 +9,15 @@ import org.json.JSONException; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; + public class JsonMatcher extends BaseMatcher { - private final String expected; + private final String expectedJson; private JSONCompareMode compareMode; - public JsonMatcher(final String expected, final JSONCompareMode compareMode) { - this.expected = expected; + public JsonMatcher(final String expectedJson, final JSONCompareMode compareMode) { + this.expectedJson = expectedJson; this.compareMode = compareMode; } @@ -47,11 +49,13 @@ public class JsonMatcher extends BaseMatcher { return false; } try { - final var actualJson = new ObjectMapper().writeValueAsString(actual); - JSONAssert.assertEquals(expected, actualJson, compareMode); + final var actualJson = new ObjectMapper().enable(INDENT_OUTPUT).writeValueAsString(actual); + JSONAssert.assertEquals(expectedJson, actualJson, compareMode); return true; } catch (final JSONException | JsonProcessingException e) { throw new AssertionError(e); + } catch (final Exception e ) { + throw e; } } -- 2.39.5 From 79c7469fef094b0bfb01a12b1ef12aadfd4ed895 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 26 Jun 2024 15:20:17 +0200 Subject: [PATCH 07/11] cleanup --- .../hs/hosting/asset/HsHostingAssetController.java | 8 +++++--- .../validators/HsHostingAssetEntityValidatorRegistry.java | 2 +- .../hsadminng/hs/validation/HsEntityValidator.java | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 163aeae1..b0e5cd62 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -127,12 +127,14 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, current).apply(body); -// validate(current)/ / self-validation, hashing passwords etc. +// TODO.refa: draft for an alternative API +// validate(current) // self-validation, hashing passwords etc. // .then(HsHostingAssetEntityValidatorRegistry::prepareForSave) // hashing passwords etc. // .then(assetRepo::save) // .then(HsHostingAssetEntityValidatorRegistry::validateInContext) -// .then(this::mapToResource) using postprocessProperties to remove write-only + add read-only properties - +// // In this last step we need the entity and the mapped resource instance, +// // which is exactly what a postmapper takes as arguments. +// .then(this::mapToResource) using postProcessProperties to remove write-only + add read-only properties final var saved = validated(assetRepo.save(current)); final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index df00c710..a5331f81 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -52,7 +52,7 @@ public class HsHostingAssetEntityValidatorRegistry { public static void postprocessProperties(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) { final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); - final var config = validator.postprocess(entity, asMap(resource)); + final var config = validator.postProcess(entity, asMap(resource)); resource.setConfig(config); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 9bc4b3cb..fb197cd7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -89,7 +89,7 @@ public abstract class HsEntityValidator { throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } - public Map postprocess(final E entity, final Map config) { + public Map postProcess(final E entity, final Map config) { final var copy = new HashMap<>(config); stream(propertyValidators).forEach(p -> { if ( p.writeOnly) { -- 2.39.5 From 32a8321c0e64d6a96a3526e9d21a71c419e4700e Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 26 Jun 2024 16:29:13 +0200 Subject: [PATCH 08/11] proper properties descriptions --- .../hs/validation/IntegerProperty.java | 2 +- .../hs/validation/StringProperty.java | 13 +++++---- .../hs/validation/ValidatableProperty.java | 28 ++++++++++++------- ...UnixUserHostingAssetValidatorUnitTest.java | 21 ++++++++++++++ 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index 37a011d4..f185c469 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -11,7 +11,7 @@ public class IntegerProperty extends ValidatableProperty { private final static String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, - Array.of("unit", "min", "max", "step"), + Array.of("unit", "min", "minFrom", "max", "maxFrom", "step"), ValidatableProperty.KEY_ORDER_TAIL); private String unit; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index 805f3d0b..616ff26c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -12,9 +12,10 @@ public class StringProperty extends ValidatableProperty { private static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, - Array.of("values"), - ValidatableProperty.KEY_ORDER_TAIL); - private Pattern regExPattern; + Array.of("matchesRegEx", "minLength", "maxLength"), + ValidatableProperty.KEY_ORDER_TAIL, + Array.of("hidden")); + private Pattern matchesRegEx; private Integer minLength; private Integer maxLength; private boolean hidden; @@ -38,7 +39,7 @@ public class StringProperty extends ValidatableProperty { } public StringProperty matchesRegEx(final String regExPattern) { - this.regExPattern = Pattern.compile(regExPattern); + this.matchesRegEx = Pattern.compile(regExPattern); return this; } @@ -55,8 +56,8 @@ public class StringProperty extends ValidatableProperty { 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 (regExPattern != null && !regExPattern.matcher(propValue).matches()) { - result.add(propertyName + "' is expected to be match " + regExPattern + " but " + display(propValue) + " does not match"); + if (matchesRegEx != null && !matchesRegEx.matcher(propValue).matches()) { + result.add(propertyName + "' is expected to be match " + matchesRegEx + " but " + display(propValue) + " does not match"); } if (readOnly && propValue != null) { result.add(propertyName + "' is readonly but given as " + display(propValue)); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index c65f9325..259bff9d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -25,7 +25,7 @@ import static java.util.Optional.ofNullable; public abstract class ValidatableProperty { protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); - protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "isTotalsValidator", "thresholdPercentage"); + protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage"); final Class type; final String propertyName; @@ -33,6 +33,7 @@ public abstract class ValidatableProperty { private Boolean required; private T defaultValue; protected Function computedBy; + protected boolean computed; // used in descriptor, because computedBy cannot be rendered to a text string protected boolean readOnly; protected boolean writeOnly; @@ -177,26 +178,32 @@ public abstract class ValidatableProperty { // Add entries according to the given order for (String key : keyOrder) { final Optional propValue = getPropertyValue(key); - propValue.ifPresent(o -> sortedMap.put(key, o)); + propValue.filter(ValidatableProperty::isToBeRendered).ifPresent(o -> sortedMap.put(key, o)); } return sortedMap; } + private static boolean isToBeRendered(final Object v) { + return !(v instanceof Boolean b) || b; + } + @SneakyThrows private Optional getPropertyValue(final String key) { + return getPropertyValue(getClass(), key); + } + + @SneakyThrows + private Optional getPropertyValue(final Class clazz, final String key) { try { - final var field = getClass().getDeclaredField(key); + final var field = clazz.getDeclaredField(key); field.setAccessible(true); return Optional.ofNullable(arrayToList(field.get(this))); - } catch (final NoSuchFieldException e1) { - try { - final var field = getClass().getSuperclass().getDeclaredField(key); - field.setAccessible(true); - return Optional.ofNullable(arrayToList(field.get(this))); - } catch (final NoSuchFieldException e2) { - return Optional.empty(); + } catch (final NoSuchFieldException exc) { + if (clazz.getSuperclass() != null) { + return getPropertyValue(clazz.getSuperclass(), key); } + throw exc; } } @@ -220,6 +227,7 @@ public abstract class ValidatableProperty { public ValidatableProperty computedBy(final Function compute) { this.computedBy = compute; + this.computed = true; return this; } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 15f2de6c..c8b818f3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -107,4 +107,25 @@ class HsUnixUserHostingAssetValidatorUnitTest { assertThat(result).containsExactly( "'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'"); } + + @Test + void describesItsProperties() { + // given + final var validator = HsHostingAssetEntityValidatorRegistry.forType(UNIX_USER); + + // when + final var props = validator.properties(); + + // then + assertThat(props).extracting(Object::toString).containsExactlyInAnyOrder( + "{type=integer, propertyName=SSD hard quota, unit=GB, maxFrom=SSD}", + "{type=integer, propertyName=SSD soft quota, unit=GB, maxFrom=SSD hard quota}", + "{type=integer, propertyName=HDD hard quota, unit=GB, maxFrom=HDD}", + "{type=integer, propertyName=HDD soft quota, unit=GB, maxFrom=HDD hard quota}", + "{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}", + "{type=string, propertyName=homedir, readOnly=true, computed=true}", + "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, hidden=true}", + "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, hidden=true}" + ); + } } -- 2.39.5 From d504347ac297e88c2afcfc82cd0980d713a8773c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 26 Jun 2024 16:39:28 +0200 Subject: [PATCH 09/11] amend test assertions to missing boolean=false props --- ...ingAssetPropsControllerAcceptanceTest.java | 108 ++++-------------- 1 file changed, 24 insertions(+), 84 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 9a04c9b4..7910408c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -59,9 +59,7 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 92, - "isTotalsValidator": false + "defaultValue": 92 }, { "type": "integer", @@ -69,9 +67,7 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 92, - "isTotalsValidator": false + "defaultValue": 92 }, { "type": "integer", @@ -79,18 +75,14 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 98, - "isTotalsValidator": false + "defaultValue": 98 }, { "type": "integer", "propertyName": "monit_min_free_ssd", "min": 1, "max": 1000, - "required": false, - "defaultValue": 5, - "isTotalsValidator": false + "defaultValue": 5 }, { "type": "integer", @@ -98,32 +90,24 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 95, - "isTotalsValidator": false + "defaultValue": 95 }, { "type": "integer", "propertyName": "monit_min_free_hdd", "min": 1, "max": 4000, - "required": false, - "defaultValue": 10, - "isTotalsValidator": false + "defaultValue": 10 }, { "type": "boolean", "propertyName": "software-pgsql", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", "propertyName": "software-mariadb", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "enumeration", @@ -139,114 +123,70 @@ class HsHostingAssetPropsControllerAcceptanceTest { "8.1", "8.2" ], - "required": false, - "defaultValue": "8.2", - "isTotalsValidator": false + "defaultValue": "8.2" }, { "type": "boolean", - "propertyName": "software-php-5.6", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-5.6" }, { "type": "boolean", - "propertyName": "software-php-7.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.0" }, { "type": "boolean", - "propertyName": "software-php-7.1", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.1" }, { "type": "boolean", - "propertyName": "software-php-7.2", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.2" }, { "type": "boolean", - "propertyName": "software-php-7.3", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.3" }, { "type": "boolean", "propertyName": "software-php-7.4", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", - "propertyName": "software-php-8.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-8.0" }, { "type": "boolean", - "propertyName": "software-php-8.1", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-8.1" }, { "type": "boolean", "propertyName": "software-php-8.2", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", - "propertyName": "software-postfix-tls-1.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-postfix-tls-1.0" }, { "type": "boolean", - "propertyName": "software-dovecot-tls-1.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-dovecot-tls-1.0" }, { "type": "boolean", "propertyName": "software-clamav", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", - "propertyName": "software-collabora", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-collabora" }, { "type": "boolean", - "propertyName": "software-libreoffice", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-libreoffice" }, { "type": "boolean", - "propertyName": "software-imagemagick-ghostscript", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-imagemagick-ghostscript" } ] """)); -- 2.39.5 From 94d96548d4ce39cada110cad36ae248773555ab7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 27 Jun 2024 09:06:30 +0200 Subject: [PATCH 10/11] cleanup --- .../java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java index 9431c184..22ddead9 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java @@ -54,8 +54,6 @@ public class JsonMatcher extends BaseMatcher { return true; } catch (final JSONException | JsonProcessingException e) { throw new AssertionError(e); - } catch (final Exception e ) { - throw e; } } @@ -63,5 +61,4 @@ public class JsonMatcher extends BaseMatcher { public void describeTo(final Description description) { description.appendText("leniently matches JSON"); } - } -- 2.39.5 From c59fb34b8b749d3e5e1f0c5592556b8e5cb7f416 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 27 Jun 2024 12:28:05 +0200 Subject: [PATCH 11/11] fixing issues from code-review --- .../HsUnixUserHostingAssetValidator.java | 2 +- .../hs/validation/EnumerationProperty.java | 8 ++-- .../hs/validation/HsEntityValidator.java | 2 +- .../hs/validation/PasswordProperty.java | 2 +- .../hs/validation/StringProperty.java | 18 ++++---- .../hs/validation/ValidatableProperty.java | 41 ++++++++++++++----- ...UnixUserHostingAssetValidatorUnitTest.java | 4 +- 7 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index 6b390767..74e59965 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -29,7 +29,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { .values("/bin/false", "/bin/bash", "/bin/csh", "/bin/dash", "/usr/bin/tcsh", "/usr/bin/zsh", "/usr/bin/passwd") .withDefault("/bin/false"), stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), - stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).hidden().writeOnly().optional(), + stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(), passwordProperty("password").minLength(8).maxLength(40).writeOnly()); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 0266e003..60af1b73 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -32,20 +32,20 @@ public class EnumerationProperty extends ValidatableProperty { } public void deferredInit(final ValidatableProperty[] allProperties) { - if (deferredInit != null) { + if (hasDeferredInit()) { if (this.values != null) { throw new IllegalStateException("property " + this + " already has values"); } - this.values = deferredInit.apply(allProperties); + this.values = doDeferredInit(allProperties); } } public ValidatableProperty valuesFromProperties(final String propertyNamePrefix) { - this.deferredInit = (ValidatableProperty[] allProperties) -> stream(allProperties) + this.setDeferredInit( (ValidatableProperty[] allProperties) -> stream(allProperties) .map(ValidatableProperty::propertyName) .filter(name -> name.startsWith(propertyNamePrefix)) .map(name -> name.substring(propertyNamePrefix.length())) - .toArray(String[]::new); + .toArray(String[]::new)); return this; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index fb197cd7..5af7118d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -92,7 +92,7 @@ public abstract class HsEntityValidator { public Map postProcess(final E entity, final Map config) { final var copy = new HashMap<>(config); stream(propertyValidators).forEach(p -> { - if ( p.writeOnly) { + if ( p.isWriteOnly()) { copy.remove(p.propertyName); } if (p.isComputed()) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index 441f0d06..92cafb9a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -10,7 +10,7 @@ public class PasswordProperty extends StringProperty { private PasswordProperty(final String propertyName) { super(propertyName); - hidden(); + undisclosed(); } public static PasswordProperty passwordProperty(final String propertyName) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index 616ff26c..a499d951 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -6,7 +6,6 @@ import net.hostsharing.hsadminng.mapper.Array; import java.util.List; import java.util.regex.Pattern; - @Setter public class StringProperty extends ValidatableProperty { @@ -14,11 +13,11 @@ public class StringProperty extends ValidatableProperty { ValidatableProperty.KEY_ORDER_HEAD, Array.of("matchesRegEx", "minLength", "maxLength"), ValidatableProperty.KEY_ORDER_TAIL, - Array.of("hidden")); + Array.of("undisclosed")); private Pattern matchesRegEx; private Integer minLength; private Integer maxLength; - private boolean hidden; + private boolean undisclosed; protected StringProperty(final String propertyName) { super(String.class, propertyName, KEY_ORDER); @@ -43,8 +42,13 @@ public class StringProperty extends ValidatableProperty { return this; } - public StringProperty hidden() { - this.hidden = true; + /** + * The property value is not disclosed in error messages. + * + * @return this; + */ + public StringProperty undisclosed() { + this.undisclosed = true; return this; } @@ -59,13 +63,13 @@ public class StringProperty extends ValidatableProperty { if (matchesRegEx != null && !matchesRegEx.matcher(propValue).matches()) { result.add(propertyName + "' is expected to be match " + matchesRegEx + " but " + display(propValue) + " does not match"); } - if (readOnly && propValue != null) { + if (isReadOnly() && propValue != null) { result.add(propertyName + "' is readonly but given as " + display(propValue)); } } private String display(final String propValue) { - return hidden ? "provided value" : ("'" + propValue + "'"); + return undisclosed ? "provided value" : ("'" + propValue + "'"); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 259bff9d..b34eb8fa 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -1,6 +1,8 @@ package net.hostsharing.hsadminng.hs.validation; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.experimental.Accessors; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; @@ -21,6 +23,7 @@ import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; +@Getter @RequiredArgsConstructor public abstract class ValidatableProperty { @@ -29,16 +32,28 @@ public abstract class ValidatableProperty { final Class type; final String propertyName; + + @JsonIgnore private final String[] keyOrder; + private Boolean required; private T defaultValue; - protected Function computedBy; - protected boolean computed; // used in descriptor, because computedBy cannot be rendered to a text string - protected boolean readOnly; - protected boolean writeOnly; - protected Function[], T[]> deferredInit; + @JsonIgnore + private Function computedBy; + + @Accessors(makeFinal = true, chain = true, fluent = false) + private boolean computed; // used in descriptor, because computedBy cannot be rendered to a text string + + @Accessors(makeFinal = true, chain = true, fluent = false) + private boolean readOnly; + + @Accessors(makeFinal = true, chain = true, fluent = false) + private boolean writeOnly; + + private Function[], T[]> deferredInit; private boolean isTotalsValidator = false; + @JsonIgnore private List>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty @@ -48,6 +63,17 @@ public abstract class ValidatableProperty { return null; } + protected void setDeferredInit(final Function[], T[]> function) { + this.deferredInit = function; + } + + public boolean hasDeferredInit() { + return deferredInit != null; + } + + public T[] doDeferredInit(final ValidatableProperty[] allProperties) { + return deferredInit.apply(allProperties); + } public ValidatableProperty writeOnly() { this.writeOnly = true; @@ -61,7 +87,6 @@ public abstract class ValidatableProperty { return this; } - public ValidatableProperty required() { required = TRUE; return this; @@ -231,10 +256,6 @@ public abstract class ValidatableProperty { return this; } - public boolean isComputed() { - return computedBy != null; - } - public T compute(final E entity) { return computedBy.apply(entity); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index c8b818f3..8ed76743 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -124,8 +124,8 @@ class HsUnixUserHostingAssetValidatorUnitTest { "{type=integer, propertyName=HDD soft quota, unit=GB, maxFrom=HDD hard quota}", "{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}", "{type=string, propertyName=homedir, readOnly=true, computed=true}", - "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, hidden=true}", - "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, hidden=true}" + "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}", + "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, undisclosed=true}" ); } } -- 2.39.5