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:
@@ -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
|
||||
|
@@ -128,8 +128,8 @@ public class HsHostingAssetEntity implements HsHostingAsset {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> directProps() {
|
||||
return config;
|
||||
public PatchableMapWrapper<Object> directProps() {
|
||||
return getConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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) + "]";
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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 + "'");
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user