1
0

import-email-addresses (#86)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/86
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-08-12 12:06:12 +02:00
parent 99a26aed8b
commit 0763511edd
34 changed files with 1027 additions and 539 deletions

View File

@@ -158,8 +158,8 @@ public class HsBookingItemEntity implements Stringifyable, BaseEntity<HsBookingI
}
@Override
public Map<String, Object> directProps() {
return resources;
public PatchableMapWrapper<Object> directProps() {
return getResources();
}
@Override

View File

@@ -128,8 +128,8 @@ public class HsHostingAssetEntity implements HsHostingAsset {
}
@Override
public Map<String, Object> directProps() {
return config;
public PatchableMapWrapper<Object> directProps() {
return getConfig();
}
@Override

View File

@@ -14,7 +14,7 @@ public class HsHostingAssetEntityPatcher implements EntityPatcher<HsHostingAsset
private final EntityManager em;
private final HsHostingAssetEntity entity;
HsHostingAssetEntityPatcher(final EntityManager em, final HsHostingAssetEntity entity) {
public HsHostingAssetEntityPatcher(final EntityManager em, final HsHostingAssetEntity entity) {
this.em = em;
this.entity = entity;
}

View File

@@ -6,8 +6,10 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.persistence.EntityManager;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
/**
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsHostingAsset into a readable API.
@@ -40,12 +42,14 @@ public class HostingAssetEntitySaveProcessor {
return this;
}
// TODO.impl: remove once the migration of legacy data is done
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String ignoreRegExp) {
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
step("validateEntity", "prepareForSave");
final var ignoreRegExpPatterns = Arrays.stream(ignoreRegExp).map(Pattern::compile).toList();
MultiValidationException.throwIfNotEmpty(
validator.validateEntity(entity).stream()
.filter(errorMsg -> !errorMsg.matches(ignoreRegExp))
.filter(error -> ignoreRegExpPatterns.stream().noneMatch(p -> p.matcher(error).matches() ))
.toList()
);
return this;

View File

@@ -11,20 +11,22 @@ import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringPrope
class HsEMailAddressHostingAssetValidator extends HostingAssetEntityValidator {
private static final String UNIX_USER_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9][a-z0-9\\._-]*)?$"; // also accepts legacy pac-names
private static final String TARGET_MAILBOX_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9][a-z0-9\\.+_-]*)?$"; // also accepts legacy pac-names
private static final String EMAIL_ADDRESS_LOCAL_PART_REGEX = "[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+"; // RFC 5322
private static final String EMAIL_ADDRESS_DOMAIN_PART_REGEX = "[a-zA-Z0-9.-]+";
private static final String EMAIL_ADDRESS_FULL_REGEX = "^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "@" + EMAIL_ADDRESS_DOMAIN_PART_REGEX + "$";
private static final String EMAIL_ADDRESS_FULL_REGEX = "^(" + EMAIL_ADDRESS_LOCAL_PART_REGEX + ")?@" + EMAIL_ADDRESS_DOMAIN_PART_REGEX + "$";
private static final String NOBODY_REGEX = "^nobody$";
private static final String DEVNULL_REGEX = "^/dev/null$";
public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322
HsEMailAddressHostingAssetValidator() {
super( HsHostingAssetType.EMAIL_ADDRESS,
AlarmContact.isOptional(),
stringProperty("local-part").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").required(),
stringProperty("sub-domain").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").optional(),
stringProperty("local-part").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").writeOnce().optional(),
stringProperty("sub-domain").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").writeOnce().optional(),
arrayOf(
stringProperty("target").maxLength(EMAIL_ADDRESS_MAX_LENGTH).matchesRegEx(UNIX_USER_REGEX, EMAIL_ADDRESS_FULL_REGEX)
stringProperty("target").maxLength(EMAIL_ADDRESS_MAX_LENGTH).matchesRegEx(TARGET_MAILBOX_REGEX, EMAIL_ADDRESS_FULL_REGEX, NOBODY_REGEX, DEVNULL_REGEX)
).required().minLength(1));
}
@@ -43,9 +45,9 @@ class HsEMailAddressHostingAssetValidator extends HostingAssetEntityValidator {
}
private static String combineIdentifier(final HsHostingAsset emailAddressAssetEntity) {
return emailAddressAssetEntity.getDirectValue("local-part", String.class) +
ofNullable(emailAddressAssetEntity.getDirectValue("sub-domain", String.class)).map(s -> "." + s).orElse("") +
"@" +
emailAddressAssetEntity.getParentAsset().getIdentifier();
return ofNullable(emailAddressAssetEntity.getDirectValue("local-part", String.class)).orElse("")
+ "@"
+ ofNullable(emailAddressAssetEntity.getDirectValue("sub-domain", String.class)).map(s -> s + ".").orElse("")
+ emailAddressAssetEntity.getParentAsset().getParentAsset().getIdentifier();
}
}

View File

@@ -43,10 +43,10 @@ public class ArrayProperty<P extends ValidatableProperty<?, E>, E> extends Valid
@Override
protected void validate(final List<String> result, final E[] propValue, final PropertiesProvider propProvider) {
if (minLength != null && propValue.length < minLength) {
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length);
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + displayArray(propValue) + " is " + propValue.length);
}
if (maxLength != null && propValue.length > maxLength) {
result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length);
result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + displayArray(propValue) + " is " + propValue.length);
}
stream(propValue).forEach(e -> elementsOf.validate(result, e, propProvider));
}
@@ -57,7 +57,7 @@ public class ArrayProperty<P extends ValidatableProperty<?, E>, E> extends Valid
}
@SafeVarargs
private String display(final E... propValue) {
private String displayArray(final E... propValue) {
return "[" + Arrays.toString(propValue) + "]";
}
}

View File

@@ -1,11 +1,11 @@
package net.hostsharing.hsadminng.hs.validation;
import java.util.Map;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
public interface PropertiesProvider {
boolean isLoaded();
Map<String, Object> directProps();
PatchableMapWrapper<Object> directProps();
Object getContextValue(final String propName);
default <T> T getDirectValue(final String propName, final Class<T> clazz) {
@@ -16,6 +16,10 @@ public interface PropertiesProvider {
return cast(propName, directProps().get(propName), clazz, defaultValue);
}
default boolean isPatched(String propertyName) {
return directProps().isPatched(propertyName);
}
default <T> T getContextValue(final String propName, final Class<T> clazz) {
return cast(propName, getContextValue(propName), clazz, null);
}

View File

@@ -77,6 +77,7 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
@Override
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
super.validate(result, propValue, propProvider);
if (minLength != null && propValue.length()<minLength) {
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length());
}
@@ -87,12 +88,10 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) {
result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match" + (matchesRegEx.length>1?" any":""));
}
if (isReadOnly() && propValue != null) {
result.add(propertyName + "' is readonly but given as " + display(propValue));
}
}
private String display(final String propValue) {
@Override
protected String display(final String propValue) {
return undisclosed ? "provided value" : ("'" + propValue + "'");
}

View File

@@ -34,7 +34,7 @@ import static org.apache.commons.lang3.ObjectUtils.isArray;
public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T> {
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "requiresAtLeastOneOf", "requiresAtMaxOneOf", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "requiresAtLeastOneOf", "requiresAtMaxOneOf", "defaultValue", "readOnly", "writeOnce","writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
protected static final String[] KEY_ORDER = Array.join(KEY_ORDER_HEAD, KEY_ORDER_TAIL);
final Class<T> type;
@@ -66,6 +66,9 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
@Accessors(makeFinal = true, chain = true, fluent = false)
private boolean writeOnly;
@Accessors(makeFinal = true, chain = true, fluent = false)
private boolean writeOnce;
private Function<ValidatableProperty<?, ?>[], T[]> deferredInit;
private boolean isTotalsValidator = false;
@@ -97,7 +100,11 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
public P writeOnly() {
this.writeOnly = true;
optional();
return self();
}
public P writeOnce() {
this.writeOnce = true;
return self();
}
@@ -198,6 +205,9 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
if (required == TRUE) {
result.add(propertyName + "' is required but missing");
}
if (isWriteOnce() && propsProvider.isLoaded() && propsProvider.isPatched(propertyName) ) {
result.add(propertyName + "' is write-once but got removed");
}
validateRequiresAtLeastOneOf(result, propsProvider);
}
if (propValue != null){
@@ -239,19 +249,35 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
}
}
protected abstract void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider);
protected void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider) {
if (isReadOnly() && propValue != null) {
result.add(propertyName + "' is readonly but given as " + display(propValue));
}
if (isWriteOnce() && propProvider.isLoaded() && propValue != null && propProvider.isPatched(propertyName) ) {
result.add(propertyName + "' is write-once but given as " + display(propValue));
}
}
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null && requiresAtLeastOneOf == null && requiresAtMaxOneOf == null && !readOnly && defaultValue == null) {
if (isSpecPotentiallyComplete()) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .readOnly(), .required(), .optional(), .withDefault(...), .requiresAtLeastOneOf(...) or .requiresAtMaxOneOf(...)" );
}
}
private boolean isSpecPotentiallyComplete() {
return required == null && requiresAtLeastOneOf == null && requiresAtMaxOneOf == null && !readOnly && !writeOnly
&& defaultValue == null;
}
@SuppressWarnings("unchecked")
public T getValue(final Map<String, Object> propValues) {
return (T) Optional.ofNullable(propValues.get(propertyName)).orElse(defaultValue);
}
protected String display(final T propValue) {
return propValue == null ? null : propValue.toString();
}
protected abstract String simpleTypeName();
public Map<String, Object> toOrderedMap() {

View File

@@ -7,7 +7,9 @@ import org.apache.commons.lang3.tuple.ImmutablePair;
import jakarta.validation.constraints.NotNull;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
@@ -23,6 +25,7 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
.configure(SerializationFeature.INDENT_OUTPUT, true);
private final Map<String, T> delegate;
private final Set<String> patched = new HashSet<>();
private PatchableMapWrapper(final Map<String, T> map) {
delegate = map;
@@ -36,6 +39,10 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
});
}
public static <T> PatchableMapWrapper<T> of(final Map<String, T> delegate) {
return new PatchableMapWrapper<T>(delegate);
}
@NotNull
public static <E> ImmutablePair<String, E> entry(final String key, final E value) {
return new ImmutablePair<>(key, value);
@@ -45,6 +52,7 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
if (entries != null ) {
delegate.clear();
delegate.putAll(entries);
patched.clear();
}
}
@@ -58,6 +66,10 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
});
}
public boolean isPatched(final String propertyName) {
return patched.contains(propertyName);
}
@SneakyThrows
public String toString() {
return jsonWriter.writeValueAsString(delegate);
@@ -92,11 +104,17 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
@Override
public T put(final String key, final T value) {
if (!Objects.equals(value, delegate.get(key))) {
patched.add(key);
}
return delegate.put(key, value);
}
@Override
public T remove(final Object key) {
if (delegate.containsKey(key.toString())) {
patched.add(key.toString());
}
return delegate.remove(key);
}
@@ -107,20 +125,24 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
@Override
public void clear() {
patched.addAll(delegate.keySet());
delegate.clear();
}
@Override
@NotNull
public Set<String> keySet() {
return delegate.keySet();
}
@Override
@NotNull
public Collection<T> values() {
return delegate.values();
}
@Override
@NotNull
public Set<Entry<String, T>> entrySet() {
return delegate.entrySet();
}

View File

@@ -189,15 +189,11 @@ begin
select g.descendantUuid, g.ascendantUuid, level + 1 as level
from RbacGrants g
inner join grants on grants.descendantUuid = g.ascendantUuid
where g.assumed
),
granted as (
select distinct descendantUuid
from grants
where g.assumed and level<10
)
select distinct perm.objectUuid as objectUuid
from granted
join RbacPermission perm on granted.descendantUuid = perm.uuid
from grants
join RbacPermission perm on grants.descendantUuid = perm.uuid
join RbacObject obj on obj.uuid = perm.objectUuid
where obj.objectTable = '%1$s' -- 'SELECT' permission is included in all other permissions
limit 8001