hosting-asset-validation-baseline (#56)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #56
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-05-30 10:45:12 +02:00
parent 2e9e5d6ef0
commit 23a6f89943
41 changed files with 1211 additions and 571 deletions

View File

@ -17,6 +17,7 @@ import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController
@ -56,7 +57,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = bookingItemRepo.save(entityToSave);
final var saved = bookingItemRepo.save(valid(entityToSave));
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -111,7 +112,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
new HsBookingItemEntityPatcher(current).apply(body);
final var saved = bookingItemRepo.save(current);
final var saved = bookingItemRepo.save(valid(current));
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}

View File

@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -65,7 +66,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsBookingItemEntity implements Stringifyable, RbacObject {
public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable<HsBookingItemEntity, HsBookingItemType> {
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
.withProp(HsBookingItemEntity::getDebitor)
@ -142,6 +143,16 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
":" + caption;
}
@Override
public String getPropertiesName() {
return "resources";
}
@Override
public Map<String, Object> getProperties() {
return resources;
}
public static RbacView rbac() {
return rbacViewFor("bookingItem", HsBookingItemEntity.class)
.withIdentityView(SQL.query("""

View File

@ -0,0 +1,50 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
@UtilityClass
public class HsBookingItemEntityValidators {
private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity, HsBookingItemType>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsCloudServerBookingItemValidator());
register(MANAGED_SERVER, new HsManagedServerBookingItemValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceBookingItemValidator());
}
private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity, HsBookingItemType> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsBookingItemEntity, HsBookingItemType> forType(final Enum<HsBookingItemType> type) {
return validators.get(type);
}
public static Set<Enum<HsBookingItemType>> types() {
return validators.keySet();
}
public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) {
final var violations = HsBookingItemEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
}

View File

@ -0,0 +1,22 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsCloudServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
HsCloudServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
);
}
}

View File

@ -0,0 +1,28 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
HsManagedServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").optional()
);
}
}

View File

@ -0,0 +1,24 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedWebspaceBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
public HsManagedWebspaceBookingItemValidator() {
super(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
integerProperty("Daemons").min(0).max(10).optional(),
booleanProperty("Online Office Server").optional()
);
}
}

View File

@ -1,6 +1,5 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
import net.hostsharing.hsadminng.context.Context;
@ -16,11 +15,12 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.validation.ValidationException;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid;
@RestController
public class HsHostingAssetController implements HsHostingAssetsApi {
@ -117,21 +117,17 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
new HsHostingAssetEntityPatcher(current).apply(body);
final var saved = assetRepo.save(current);
final var saved = assetRepo.save(valid(current));
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
return ResponseEntity.ok(mapped);
}
private HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
final var violations = HsHostingAssetValidator.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
@SuppressWarnings("unchecked")
final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putConfig(KeyValueMap.from(resource.getConfig()));
if (resource.getParentAssetUuid() != null) {
entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted(
resource.getParentAssetUuid()))));
}
};
}

View File

@ -8,6 +8,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -40,7 +41,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCas
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
@ -61,7 +61,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsHostingAssetEntity implements Stringifyable, RbacObject {
public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validatable<HsHostingAssetEntity, HsHostingAssetType> {
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
.withProp(HsHostingAssetEntity::getType)
@ -114,6 +114,16 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
}
@Override
public String getPropertiesName() {
return "config";
}
@Override
public Map<String, Object> getProperties() {
return config;
}
@Override
public String toString() {
return stringify.apply(this);
@ -137,7 +147,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
.importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(),
dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
NULLABLE)
.switchOnColumn("type",
inCaseOf(CLOUD_SERVER.name(),

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import org.springframework.http.ResponseEntity;
@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override
public ResponseEntity<List<String>> listAssetTypes() {
final var resource = HsHostingAssetValidator.types().stream()
final var resource = HsHostingAssetEntityValidators.types().stream()
.map(Enum::name)
.toList();
return ResponseEntity.ok(resource);
@ -25,7 +25,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
public ResponseEntity<List<Object>> listAssetTypeProps(
final HsHostingAssetTypeResource assetType) {
final var propValidators = HsHostingAssetValidator.forType(HsHostingAssetType.of(assetType));
final var propValidators = HsHostingAssetEntityValidators.forType(HsHostingAssetType.of(assetType));
final List<Map<String, Object>> resource = propValidators.properties();
return ResponseEntity.ok(toListOfObjects(resource));
}

View File

@ -1,172 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validator;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@RequiredArgsConstructor
public abstract class HsHostingAssetPropertyValidator<T> {
final Class<T> type;
final String propertyName;
private Boolean required;
public static <K, V> Map.Entry<K, V> defType(K k, V v) {
return new SimpleImmutableEntry<>(k, v);
}
public HsHostingAssetPropertyValidator<T> required() {
required = Boolean.TRUE;
return this;
}
public HsHostingAssetPropertyValidator<T> optional() {
required = Boolean.FALSE;
return this;
}
public final List<String> validate(final Map<String, Object> props) {
final var result = new ArrayList<String>();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
result.add("'" + propertyName + "' is required but missing");
}
}
if (propValue != null){
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, (T) propValue, props);
} else {
result.add("'" + propertyName + "' is expected to be of type " + type + ", " +
"but is of type '" + propValue.getClass().getSimpleName() + "'");
}
}
return result;
}
protected abstract void validate(final ArrayList<String> result, final T propValue, final Map<String, Object> props);
public void verifyConsistency(final Map.Entry<HsHostingAssetType, HsHostingAssetValidator> typeDef) {
if (required == null ) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
}
}
public Map<String, Object> toMap(final ObjectMapper mapper) {
final Map<String, Object> map = mapper.convertValue(this, Map.class);
map.put("type", simpleTypeName());
return map;
}
protected abstract String simpleTypeName();
}
@Setter
class IntegerPropertyValidator extends HsHostingAssetPropertyValidator<Integer>{
private String unit;
private Integer min;
private Integer max;
private Integer step;
public static IntegerPropertyValidator integerProperty(final String propertyName) {
return new IntegerPropertyValidator(propertyName);
}
private IntegerPropertyValidator(final String propertyName) {
super(Integer.class, propertyName);
}
@Override
protected void validate(final ArrayList<String> result, final Integer propValue, final Map<String, Object> props) {
if (min != null && propValue < min) {
result.add("'" + propertyName + "' is expected to be >= " + min + " but is " + propValue);
}
if (max != null && propValue > max) {
result.add("'" + propertyName + "' is expected to be <= " + max + " but is " + propValue);
}
if (step != null && propValue % step != 0) {
result.add("'" + propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
}
}
@Override
protected String simpleTypeName() {
return "integer";
}
}
@Setter
class EnumPropertyValidator extends HsHostingAssetPropertyValidator<String> {
private String[] values;
private EnumPropertyValidator(final String propertyName) {
super(String.class, propertyName);
}
public static EnumPropertyValidator enumerationProperty(final String propertyName) {
return new EnumPropertyValidator(propertyName);
}
public HsHostingAssetPropertyValidator<String> values(final String... values) {
this.values = values;
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
result.add("'" + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}
}
@Override
protected String simpleTypeName() {
return "enumeration";
}
}
@Setter
class BooleanPropertyValidator extends HsHostingAssetPropertyValidator<Boolean> {
private Map.Entry<String, String> falseIf;
private BooleanPropertyValidator(final String propertyName) {
super(Boolean.class, propertyName);
}
public static BooleanPropertyValidator booleanProperty(final String propertyName) {
return new BooleanPropertyValidator(propertyName);
}
HsHostingAssetPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final Boolean propValue, final Map<String, Object> props) {
if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
if (propValue) {
result.add("'" + propertyName + "' is expected to be false because " +
falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -1,99 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validator;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.EnumPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetPropertyValidator.defType;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.IntegerPropertyValidator.integerProperty;
public class HsHostingAssetValidator {
private static final Map<HsHostingAssetType, HsHostingAssetValidator> validators = Map.ofEntries(
defType(HsHostingAssetType.CLOUD_SERVER, new HsHostingAssetValidator(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional())),
defType(HsHostingAssetType.MANAGED_SERVER, new HsHostingAssetValidator(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").optional())),
defType(HsHostingAssetType.MANAGED_WEBSPACE, new HsHostingAssetValidator(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
integerProperty("Daemons").min(0).max(10).optional(),
booleanProperty("Online Office Server").optional())
));
static {
validators.entrySet().forEach(typeDef -> {
stream(typeDef.getValue().propertyValidators).forEach( entry -> {
entry.verifyConsistency(typeDef);
});
});
}
private final HsHostingAssetPropertyValidator<?>[] propertyValidators;
public static HsHostingAssetValidator forType(final HsHostingAssetType type) {
return validators.get(type);
}
HsHostingAssetValidator(final HsHostingAssetPropertyValidator<?>... validators) {
propertyValidators = validators;
}
public static Set<HsHostingAssetType> types() {
return validators.keySet();
}
public List<String> validate(final HsHostingAssetEntity assetEntity) {
final var result = new ArrayList<String>();
assetEntity.getConfig().keySet().forEach( givenPropName -> {
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
result.add("'" + givenPropName + "' is not expected but is '" +assetEntity.getConfig().get(givenPropName) + "'");
}
});
stream(propertyValidators).forEach(pv -> {
result.addAll(pv.validate(assetEntity.getConfig()));
});
return result;
}
public List<Map<String, Object>> properties() {
final var mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return Arrays.stream(propertyValidators)
.map(propertyValidator -> propertyValidator.toMap(mapper))
.map(HsHostingAssetValidator::asKeyValueMap)
.toList();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Map<String, Object> asKeyValueMap(final Map map) {
return (Map<String, Object>) map;
}
}

View File

@ -0,0 +1,20 @@
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.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsCloudServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
public HsCloudServerHostingAssetValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required()
);
}
}

View File

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
@UtilityClass
public class HsHostingAssetEntityValidators {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsCloudServerHostingAssetValidator());
register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> forType(final Enum<HsHostingAssetType> type) {
return validators.get(type);
}
public static Set<Enum<HsHostingAssetType>> types() {
return validators.keySet();
}
public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
final var violations = HsHostingAssetEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
}

View File

@ -0,0 +1,20 @@
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.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
public HsManagedServerHostingAssetValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required()
);
}
}

View File

@ -0,0 +1,34 @@
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.HsEntityValidator;
import java.util.List;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
public HsManagedWebspaceHostingAssetValidator() {
super(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required()
);
}
@Override
public List<String> validate(final HsHostingAssetEntity assetEntity) {
final var result = super.validate(assetEntity);
validateIdentifierPattern(result, assetEntity);
return result;
}
private static void validateIdentifierPattern(final List<String> result, final HsHostingAssetEntity assetEntity) {
final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getDebitor().getDefaultPrefix() + "[0-9][0-9]$";
if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) {
result.add("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
}
}
}

View File

@ -0,0 +1,42 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
@Setter
public class BooleanPropertyValidator extends HsPropertyValidator<Boolean> {
private Map.Entry<String, String> falseIf;
private BooleanPropertyValidator(final String propertyName) {
super(Boolean.class, propertyName);
}
public static BooleanPropertyValidator booleanProperty(final String propertyName) {
return new BooleanPropertyValidator(propertyName);
}
public HsPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final Boolean propValue, final Map<String, Object> props) {
if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
if (propValue) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be false because " +
propertiesName+"." + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -0,0 +1,38 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
@Setter
public class EnumerationPropertyValidator extends HsPropertyValidator<String> {
private String[] values;
private EnumerationPropertyValidator(final String propertyName) {
super(String.class, propertyName);
}
public static EnumerationPropertyValidator enumerationProperty(final String propertyName) {
return new EnumerationPropertyValidator(propertyName);
}
public HsPropertyValidator<String> values(final String... values) {
this.values = values;
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final String propValue, final Map<String, Object> props) {
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}
}
@Override
protected String simpleTypeName() {
return "enumeration";
}
}

View File

@ -0,0 +1,49 @@
package net.hostsharing.hsadminng.hs.validation;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.stream;
public class HsEntityValidator<E extends Validatable<E, T>, T extends Enum<T>> {
public final HsPropertyValidator<?>[] propertyValidators;
public HsEntityValidator(final HsPropertyValidator<?>... validators) {
propertyValidators = validators;
}
public List<String> validate(final E assetEntity) {
final var result = new ArrayList<String>();
assetEntity.getProperties().keySet().forEach( givenPropName -> {
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
result.add("'"+assetEntity.getPropertiesName()+"." + givenPropName + "' is not expected but is set to '" +assetEntity.getProperties().get(givenPropName) + "'");
}
});
stream(propertyValidators).forEach(pv -> {
result.addAll(pv.validate(assetEntity.getPropertiesName(), assetEntity.getProperties()));
});
return result;
}
public List<Map<String, Object>> properties() {
final var mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return Arrays.stream(propertyValidators)
.map(propertyValidator -> propertyValidator.toMap(mapper))
.map(HsEntityValidator::asKeyValueMap)
.toList();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Map<String, Object> asKeyValueMap(final Map map) {
return (Map<String, Object>) map;
}
}

View File

@ -0,0 +1,67 @@
package net.hostsharing.hsadminng.hs.validation;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
public abstract class HsPropertyValidator<T> {
final Class<T> type;
final String propertyName;
private Boolean required;
public static <K, V> Map.Entry<K, V> defType(K k, V v) {
return new SimpleImmutableEntry<>(k, v);
}
public HsPropertyValidator<T> required() {
required = Boolean.TRUE;
return this;
}
public HsPropertyValidator<T> optional() {
required = Boolean.FALSE;
return this;
}
public final List<String> validate(final String propertiesName, final Map<String, Object> props) {
final var result = new ArrayList<String>();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
result.add("'"+propertiesName+"." + propertyName + "' is required but missing");
}
}
if (propValue != null){
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, propertiesName, (T) propValue, props);
} else {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be of type " + type + ", " +
"but is of type '" + propValue.getClass().getSimpleName() + "'");
}
}
return result;
}
protected abstract void validate(final ArrayList<String> result, final String propertiesName, final T propValue, final Map<String, Object> props);
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null ) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
}
}
public Map<String, Object> toMap(final ObjectMapper mapper) {
final Map<String, Object> map = mapper.convertValue(this, Map.class);
map.put("type", simpleTypeName());
return map;
}
protected abstract String simpleTypeName();
}

View File

@ -0,0 +1,42 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Map;
@Setter
public class IntegerPropertyValidator extends HsPropertyValidator<Integer> {
private String unit;
private Integer min;
private Integer max;
private Integer step;
public static IntegerPropertyValidator integerProperty(final String propertyName) {
return new IntegerPropertyValidator(propertyName);
}
private IntegerPropertyValidator(final String propertyName) {
super(Integer.class, propertyName);
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final Integer propValue, final Map<String, Object> props) {
if (min != null && propValue < min) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be >= " + min + " but is " + propValue);
}
if (max != null && propValue > max) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be <= " + max + " but is " + propValue);
}
if (step != null && propValue % step != 0) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
}
}
@Override
protected String simpleTypeName() {
return "integer";
}
}

View File

@ -0,0 +1,13 @@
package net.hostsharing.hsadminng.hs.validation;
import java.util.Map;
public interface Validatable<E, T extends Enum<T>> {
Enum<T> getType();
String getPropertiesName();
Map<String, Object> getProperties();
}

View File

@ -8,6 +8,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.validation.constraints.NotNull;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
@ -25,6 +26,7 @@ public class RbacGrantsDiagramService {
public static void writeToFile(final String title, final String graph, final String fileName) {
new File("doc/temp").mkdirs();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
writer.write("""
### all grants to %s
@ -192,8 +194,9 @@ public class RbacGrantsDiagramService {
return "[" + roleType + "\nref:" + uuid + "]";
}
if (refType.equals("perm")) {
final var roleType = idName.split(":")[1];
return "{{" + roleType + "\nref:" + uuid + "}}";
final var parts = idName.split(":");
final var permType = parts[2];
return "{{" + permType + "\nref:" + uuid + "}}";
}
return "";
}
@ -205,7 +208,7 @@ public class RbacGrantsDiagramService {
@NotNull
private static String cleanId(final String idName) {
return idName.replaceAll("@.*", "")
.replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "");
.replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "").replace(">", ":");
}

View File

@ -53,7 +53,11 @@ components:
bookingItemUuid:
type: string
format: uuid
nullable: false
nullable: true
parentAssetUuid:
type: string
format: uuid
nullable: true
type:
$ref: '#/components/schemas/HsHostingAssetType'
identifier:
@ -72,7 +76,6 @@ components:
- type
- identifier
- caption
- debitorUuid
- config
additionalProperties: false

View File

@ -32,9 +32,9 @@ begin
raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
insert
into hs_booking_item (uuid, debitoruuid, type, caption, validity, resources)
values (uuid_generate_v4(), relatedDebitor.uuid, 'MANAGED_SERVER', 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPU": 2, "SDD": 512, "extra": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'CLOUD_SERVER', 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'PRIVATE_CLOUD', 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPU": 10, "SDD": 10240, "HDD": 10240, "extra": 42 }'::jsonb);
values (uuid_generate_v4(), relatedDebitor.uuid, 'MANAGED_SERVER', 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'CLOUD_SERVER', 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'PRIVATE_CLOUD', 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::jsonb);
end; $$;
--//

View File

@ -24,12 +24,14 @@ create table if not exists hs_hosting_asset
(
uuid uuid unique references RbacObject (uuid),
version int not null default 0,
bookingItemUuid uuid not null references hs_booking_item(uuid),
bookingItemUuid uuid null references hs_booking_item(uuid),
type HsHostingAssetType not null,
parentAssetUuid uuid null references hs_hosting_asset(uuid),
identifier varchar(80) not null,
caption varchar(80) not null,
config jsonb not null
config jsonb not null,
constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset check (bookingItemUuid is not null or parentAssetUuid is not null)
);
--//

View File

@ -39,8 +39,6 @@ begin
SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentServer;
SELECT * FROM hs_booking_item WHERE uuid = NEW.bookingItemUuid INTO newBookingItem;
assert newBookingItem.uuid is not null, format('newBookingItem must not be null for NEW.bookingItemUuid = %s', NEW.bookingItemUuid);
perform createRoleWithGrants(
hsHostingAssetOWNER(NEW),

View File

@ -44,10 +44,10 @@ begin
raise notice 'creating test hosting-asset: %', givenPartnerNumber::text || givenDebitorSuffix::text;
raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
insert into hs_hosting_asset
(uuid, bookingitemuuid, type, parentAssetUuid, identifier, caption, config)
(uuid, bookingitemuuid, type, parentAssetUuid, identifier, caption, config)
values (managedServerUuid, relatedPrivateCloudBookingItem.uuid, 'MANAGED_SERVER', null, 'vm10' || givenDebitorSuffix, 'some ManagedServer', '{ "CPU": 2, "SDD": 512, "extra": 42 }'::jsonb),
(uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, 'vm20' || givenDebitorSuffix, 'another CloudServer', '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb),
(uuid_generate_v4(), relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, givenWebspacePrefix || '01', 'some Webspace', '{ "RAM": 1, "SDD": 512, "HDD": 2048, "extra": 42 }'::jsonb);
(uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, 'vm20' || givenDebitorSuffix, 'another CloudServer', '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb),
(uuid_generate_v4(), relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, givenWebspacePrefix || '01', 'some Webspace', '{ "RAM": 1, "SDD": 512, "HDD": 2048, "extra": 42 }'::jsonb);
end; $$;
--//