add-unix-user-hosting-asset-validation #66

Merged
hsh-michaelhoennig merged 11 commits from add-unix-user-hosting-asset-validation into master 2024-06-27 12:39:45 +02:00
8 changed files with 204 additions and 20 deletions
Showing only changes of commit cf6bcc0b94 - Show all commits

View File

@ -47,7 +47,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
@Override @Override
public List<String> validate(final HsHostingAssetEntity assetEntity) { public List<String> validate(final HsHostingAssetEntity assetEntity) {
return sequentiallyValidate( return sequentiallyValidate(
() -> validateEntityReferences(assetEntity), () -> validateEntityReferencesAndProperties(assetEntity),
() -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem () -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem
() -> optionallyValidate(assetEntity.getBookingItem()), () -> optionallyValidate(assetEntity.getBookingItem()),
() -> optionallyValidate(assetEntity.getParentAsset()), () -> optionallyValidate(assetEntity.getParentAsset()),
@ -55,7 +55,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
); );
} }
private List<String> validateEntityReferences(final HsHostingAssetEntity assetEntity) { private List<String> validateEntityReferencesAndProperties(final HsHostingAssetEntity assetEntity) {
return Stream.of( return Stream.of(
validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate), validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate),
validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate), validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate),

View File

@ -5,14 +5,28 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.regex.Pattern; 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 { class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
HsUnixUserHostingAssetValidator() { HsUnixUserHostingAssetValidator() {
super( BookingItem.mustBeNull(), super( BookingItem.mustBeNull(),
ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE),
AssignedToAsset.mustBeNull(), AssignedToAsset.mustBeNull(),
AlarmContact.isOptional(), // TODO.spec: for quota notifications AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES); // TODO.spec: yet to be specified
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 @Override

View File

@ -35,7 +35,7 @@ public class EnumerationProperty extends ValidatableProperty<String> {
public void deferredInit(final ValidatableProperty<?>[] allProperties) { public void deferredInit(final ValidatableProperty<?>[] allProperties) {
if (deferredInit != null) { if (deferredInit != null) {
if (this.values != 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); this.values = deferredInit.apply(allProperties);
} }

View File

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

View File

@ -16,7 +16,9 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
private String unit; private String unit;
private Integer min; private Integer min;
private String minFrom;
private Integer max; private Integer max;
private String maxFrom;
private Integer step; private Integer step;
public static IntegerProperty integerProperty(final String propertyName) { public static IntegerProperty integerProperty(final String propertyName) {
@ -27,6 +29,16 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
super(Integer.class, propertyName, KEY_ORDER); 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 @Override
public String unit() { public String unit() {
return unit; return unit;

View File

@ -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<String> {
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;
}
hsh-michaelhoennig marked this conversation as resolved Outdated

doku

doku
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<String> result, final String propValue, final Map<String, Object> props) {
if (minLength != null && propValue.length()<minLength) {
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of '" + propValue+ "' is " + propValue.length());
}
if (maxLength != 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";
}
}

View File

@ -12,21 +12,35 @@ import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.
@UtilityClass @UtilityClass
public class TestHsBookingItem { 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() public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
.project(TEST_PROJECT) .project(TEST_PROJECT)
.type(HsBookingItemType.CLOUD_SERVER) .type(HsBookingItemType.CLOUD_SERVER)
.caption("test cloud server booking item") .caption("test cloud server booking item")
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build(); .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();
} }

View File

@ -1,14 +1,78 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators; package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import org.junit.jupiter.api.Test; 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.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.mapper.PatchMap.entry;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
class HsUnixUserHostingAssetValidatorUnitTest { 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 @Test
void validatesInvalidIdentifier() { void validatesInvalidIdentifier() {
// given // given
@ -19,7 +83,6 @@ class HsUnixUserHostingAssetValidatorUnitTest {
.build(); .build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
hsh-michaelhoennig marked this conversation as resolved
Review

evtl. nur false, bash, csh, ...

evtl. nur false, bash, csh, ...
// when // when
final var result = validator.validate(unixUserHostingAsset); final var result = validator.validate(unixUserHostingAsset);