add-unix-user-hosting-asset-validation #66
@ -71,7 +71,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
|
|
||||||
final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
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 =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
@ -126,7 +126,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
|
|
||||||
new HsHostingAssetEntityPatcher(em, current).apply(body);
|
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);
|
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
@ -144,4 +144,12 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
resource.getParentAssetUuid()))));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,13 @@ public class HsHostingAssetEntityValidatorRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> doValidate(final HsHostingAssetEntity hostingAsset) {
|
public static List<String> 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) {
|
public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) {
|
||||||
|
@ -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.EnumerationProperty.enumerationProperty;
|
||||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
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;
|
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||||
|
|
||||||
class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
|
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")
|
.values("/bin/false", "/bin/bash", "/bin/csh", "/bin/dash", "/usr/bin/tcsh", "/usr/bin/zsh", "/usr/bin/passwd")
|
||||||
.withDefault("/bin/false"),
|
.withDefault("/bin/false"),
|
||||||
stringProperty("homedir").readOnly(),
|
stringProperty("homedir").readOnly(),
|
||||||
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).writeOnly().optional(),
|
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).hidden().writeOnly().optional(),
|
||||||
stringProperty("password").minLength(8).maxLength(40).writeOnly()); // FIXME: spec
|
passwordProperty("password").minLength(8).maxLength(40).writeOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -4,7 +4,7 @@ import lombok.Setter;
|
|||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.ArrayList;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ public class BooleanProperty extends ValidatableProperty<Boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final Boolean propValue, final PropertiesProvider propProvider) {
|
protected void validate(final List<String> result, final Boolean propValue, final PropertiesProvider propProvider) {
|
||||||
if (falseIf != null && propValue) {
|
if (falseIf != null && propValue) {
|
||||||
final Object referencedValue = propProvider.directProps().get(falseIf.getKey());
|
final Object referencedValue = propProvider.directProps().get(falseIf.getKey());
|
||||||
if (Objects.equals(referencedValue, falseIf.getValue())) {
|
if (Objects.equals(referencedValue, falseIf.getValue())) {
|
||||||
|
@ -3,8 +3,8 @@ package net.hostsharing.hsadminng.hs.validation;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ public class EnumerationProperty extends ValidatableProperty<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final String propValue, final PropertiesProvider propProvider) {
|
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||||
if (stream(values).noneMatch(v -> v.equals(propValue))) {
|
if (stream(values).noneMatch(v -> v.equals(propValue))) {
|
||||||
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
|
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import lombok.Setter;
|
|||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.List;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
public class IntegerProperty extends ValidatableProperty<Integer> {
|
public class IntegerProperty extends ValidatableProperty<Integer> {
|
||||||
@ -55,7 +55,7 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final Integer propValue, final PropertiesProvider propProvider) {
|
protected void validate(final List<String> result, final Integer propValue, final PropertiesProvider propProvider) {
|
||||||
validateMin(result, propertyName, propValue, min);
|
validateMin(result, propertyName, propValue, min);
|
||||||
validateMax(result, propertyName, propValue, max);
|
validateMax(result, propertyName, propValue, max);
|
||||||
if (step != null && propValue % step != 0) {
|
if (step != null && propValue % step != 0) {
|
||||||
@ -74,13 +74,13 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
|
|||||||
return "integer";
|
return "integer";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateMin(final ArrayList<String> result, final String propertyName, final Integer propValue, final Integer min) {
|
private static void validateMin(final List<String> result, final String propertyName, final Integer propValue, final Integer min) {
|
||||||
if (min != null && propValue < min) {
|
if (min != null && propValue < min) {
|
||||||
result.add(propertyName + "' is expected to be at least " + min + " but is " + propValue);
|
result.add(propertyName + "' is expected to be at least " + min + " but is " + propValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateMax(final ArrayList<String> result, final String propertyName, final Integer propValue, final Integer max) {
|
private static void validateMax(final List<String> result, final String propertyName, final Integer propValue, final Integer max) {
|
||||||
if (max != null && propValue > max) {
|
if (max != null && propValue > max) {
|
||||||
result.add(propertyName + "' is expected to be at most " + max + " but is " + propValue);
|
result.add(propertyName + "' is expected to be at most " + max + " but is " + propValue);
|
||||||
}
|
}
|
||||||
|
@ -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<String> 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<String> 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 (':')");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface PropertiesProvider {
|
||||||
|
|
||||||
|
Map<String, Object> directProps();
|
||||||
|
Object getContextValue(final String propName);
|
||||||
|
|
||||||
|
default <T> T getDirectValue(final String propName, final Class<T> clazz) {
|
||||||
|
return cast(propName, directProps().get(propName), clazz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T> T getContextValue(final String propName, final Class<T> clazz) {
|
||||||
|
return cast(propName, getContextValue(propName), clazz, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T> T getContextValue(final String propName, final Class<T> clazz, final T defaultValue) {
|
||||||
|
return cast(propName, getContextValue(propName), clazz, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T cast( final String propName, final Object value, final Class<T> 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 + "'");
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.validation;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
|
||||||
@ -19,8 +19,9 @@ public class StringProperty extends ValidatableProperty<String> {
|
|||||||
private Integer maxLength;
|
private Integer maxLength;
|
||||||
private boolean writeOnly;
|
private boolean writeOnly;
|
||||||
private boolean readOnly;
|
private boolean readOnly;
|
||||||
|
private boolean hidden;
|
||||||
|
|
||||||
private StringProperty(final String propertyName) {
|
protected StringProperty(final String propertyName) {
|
||||||
super(String.class, propertyName, KEY_ORDER);
|
super(String.class, propertyName, KEY_ORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +44,11 @@ public class StringProperty extends ValidatableProperty<String> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
hsh-michaelhoennig marked this conversation as resolved
Outdated
|
|||||||
|
public StringProperty hidden() {
|
||||||
|
this.hidden = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public StringProperty writeOnly() {
|
public StringProperty writeOnly() {
|
||||||
this.writeOnly = true;
|
this.writeOnly = true;
|
||||||
super.optional();
|
super.optional();
|
||||||
@ -56,21 +62,25 @@ public class StringProperty extends ValidatableProperty<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final String propValue, final PropertiesProvider propProvider) {
|
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||||
if (minLength != null && propValue.length()<minLength) {
|
if (minLength != null && propValue.length()<minLength) {
|
||||||
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of '" + propValue+ "' is " + propValue.length());
|
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length());
|
||||||
}
|
}
|
||||||
if (maxLength != null && propValue.length()>maxLength) {
|
if (maxLength != 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()) {
|
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) {
|
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
|
@Override
|
||||||
protected String simpleTypeName() {
|
protected String simpleTypeName() {
|
||||||
return "string";
|
return "string";
|
||||||
|
@ -137,7 +137,7 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void validate(final ArrayList<String> result, final T propValue, final PropertiesProvider propProvider);
|
protected abstract void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider);
|
||||||
|
|
||||||
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
|
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
|
||||||
if (required == null ) {
|
if (required == null ) {
|
||||||
|
@ -53,13 +53,20 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "{ "
|
return "{\n"
|
||||||
+ (
|
+ (
|
||||||
keySet().stream().sorted()
|
keySet().stream().sorted()
|
||||||
.map(k -> k + ": " + get(k)))
|
.map(k -> " \"" + k + "\": " + optionallyQuoted(get(k))))
|
||||||
.collect(joining(", ")
|
.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 --------------------------------
|
// --- below just delegating methods --------------------------------
|
||||||
|
@ -25,12 +25,14 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static java.util.Map.entry;
|
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_SERVER;
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
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.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
|
||||||
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
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.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.matchesRegex;
|
import static org.hamcrest.Matchers.matchesRegex;
|
||||||
|
|
||||||
@ -264,7 +266,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
void propertyValidationsArePerformend_whenAddingAsset() {
|
void propertyValidationsArePerformend_whenAddingAsset() {
|
||||||
|
|
||||||
context.define("superuser-alex@hostsharing.net");
|
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,
|
HsBookingItemType.MANAGED_SERVER,
|
||||||
"some PrivateCloud");
|
"some PrivateCloud");
|
||||||
|
|
||||||
@ -299,7 +302,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
""".replaceAll(" +<<<", ""))); // @formatter:on
|
""".replaceAll(" +<<<", ""))); // @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void totalsLimitValidationsArePerformend_whenAddingAsset() {
|
void totalsLimitValidationsArePerformend_whenAddingAsset() {
|
||||||
|
|
||||||
@ -311,7 +313,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
jpaAttempt.transacted(() -> {
|
jpaAttempt.transacted(() -> {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
for (int n = 0; n < 25; ++n ) {
|
for (int n = 0; n < 25; ++n) {
|
||||||
toCleanup(assetRepo.save(
|
toCleanup(assetRepo.save(
|
||||||
HsHostingAssetEntity.builder()
|
HsHostingAssetEntity.builder()
|
||||||
.type(UNIX_USER)
|
.type(UNIX_USER)
|
||||||
@ -429,8 +431,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
@Test
|
@Test
|
||||||
void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() {
|
void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() {
|
||||||
|
|
||||||
final var givenAsset = givenSomeTemporaryHostingAsset("2001", MANAGED_SERVER,
|
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
|
||||||
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
|
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.<String, Object>entry("monit_max_ssd_usage", 80),
|
||||||
|
Map.<String, Object>entry("monit_max_hdd_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_cpu_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_ram_usage", 70)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
final var alarmContactUuid = givenContact().getUuid();
|
final var alarmContactUuid = givenContact().getUuid();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
@ -474,6 +491,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// finally, the asset is actually updated
|
// finally, the asset is actually updated
|
||||||
|
em.clear();
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get()
|
assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get()
|
||||||
.matches(asset -> {
|
.matches(asset -> {
|
||||||
@ -484,13 +502,76 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private HsOfficeContactEntity givenContact() {
|
@Test
|
||||||
return jpaAttempt.transacted(() -> {
|
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");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow();
|
return assetRepo.findByUuid(givenAsset.getUuid());
|
||||||
}).returnedValue();
|
}).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
|
@Nested
|
||||||
@ -500,9 +581,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
@Test
|
@Test
|
||||||
void globalAdmin_canDeleteArbitraryAsset() {
|
void globalAdmin_canDeleteArbitraryAsset() {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenAsset = givenSomeTemporaryHostingAsset("1002", MANAGED_SERVER,
|
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
|
||||||
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
|
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.<String, Object>entry("monit_max_ssd_usage", 80),
|
||||||
|
Map.<String, Object>entry("monit_max_hdd_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_cpu_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_ram_usage", 70)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("current-user", "superuser-alex@hostsharing.net")
|
.header("current-user", "superuser-alex@hostsharing.net")
|
||||||
@ -519,9 +614,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
@Test
|
@Test
|
||||||
void normalUser_canNotDeleteUnrelatedAsset() {
|
void normalUser_canNotDeleteUnrelatedAsset() {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenAsset = givenSomeTemporaryHostingAsset("1003", MANAGED_SERVER,
|
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
|
||||||
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
|
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.<String, Object>entry("monit_max_ssd_usage", 80),
|
||||||
|
Map.<String, Object>entry("monit_max_hdd_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_cpu_usage", 90),
|
||||||
|
Map.<String, Object>entry("monit_max_ram_usage", 70)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("current-user", "selfregistered-user-drew@hostsharing.org")
|
.header("current-user", "selfregistered-user-drew@hostsharing.org")
|
||||||
@ -538,7 +647,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) {
|
HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) {
|
||||||
return assetRepo.findByIdentifier(identifier).stream()
|
return assetRepo.findByIdentifier(identifier).stream()
|
||||||
.filter(ha -> ha.getType()==type)
|
.filter(ha -> ha.getType() == type)
|
||||||
.findAny().orElseThrow();
|
.findAny().orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,12 +668,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}).assertSuccessful().returnedValue();
|
}).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(() -> {
|
return jpaAttempt.transacted(() -> {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var project = projectRepo.findByCaption(projectCaption).getFirst();
|
final var project = projectRepo.findByCaption(projectCaption).getFirst();
|
||||||
final var resources = switch (bookingItemType) {
|
final var resources = switch (bookingItemType) {
|
||||||
case MANAGED_SERVER -> Map.<String, Object>ofEntries(entry("CPUs", 1), entry("RAM", 20), entry("SSD", 25), entry("Traffic", 250));
|
case MANAGED_SERVER -> Map.<String, Object>ofEntries(entry("CPUs", 1),
|
||||||
|
entry("RAM", 20),
|
||||||
|
entry("SSD", 25),
|
||||||
|
entry("Traffic", 250));
|
||||||
default -> new HashMap<String, Object>();
|
default -> new HashMap<String, Object>();
|
||||||
};
|
};
|
||||||
final var newBookingItem = HsBookingItemEntity.builder()
|
final var newBookingItem = HsBookingItemEntity.builder()
|
||||||
@ -584,33 +699,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
return givenAsset;
|
return givenAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final Supplier<HsHostingAssetEntity> newAsset) {
|
||||||
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix,
|
|
||||||
final HsHostingAssetType hostingAssetType,
|
|
||||||
final Map.Entry<String, Object>... config) {
|
|
||||||
return jpaAttempt.transacted(() -> {
|
return jpaAttempt.transacted(() -> {
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var bookingItemType = switch (hostingAssetType) {
|
return toCleanup(assetRepo.save(newAsset.get()));
|
||||||
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);
|
|
||||||
}).assertSuccessful().returnedValue();
|
}).assertSuccessful().returnedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map.Entry<String, Object> config(final String key, final Object value) {
|
private HsOfficeContactEntity givenContact() {
|
||||||
return entry(key, value);
|
return jpaAttempt.transacted(() -> {
|
||||||
|
context.define("superuser-alex@hostsharing.net");
|
||||||
|
return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow();
|
||||||
|
}).returnedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.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.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'",
|
||||||
hsh-michaelhoennig marked this conversation as resolved
hsh-marcsandlus
commented
evtl. nur false, bash, csh, ... evtl. nur false, bash, csh, ...
|
|||||||
"'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'",
|
"'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'",
|
||||||
"'UNIX_USER:abc00-temp.config.totpKey' is expected to be match ^0x([0-9A-Fa-f]{2})+$ but 'should be a hex number' does not match",
|
"'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 'short' is 5"
|
"'UNIX_USER:abc00-temp.config.password' length is expected to be at min 8 but length of provided value is 5",
|
||||||
|
"'UNIX_USER:abc00-temp.config.password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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<String> passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly();
|
||||||
|
private final List<String> 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user
doku