From 3566cb61b6c114677dbce270043dd2b06408b5b5 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 25 Jun 2024 17:12:48 +0200 Subject: [PATCH] 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); + } +}