JSonDeserializerWithAccessFilter with working access rights validation
This commit is contained in:
		@@ -3,9 +3,9 @@ package org.hostsharing.hsadminng.service.accessfilter;
 | 
			
		||||
 | 
			
		||||
import java.lang.annotation.*;
 | 
			
		||||
 | 
			
		||||
@Documented
 | 
			
		||||
@Target({ElementType.FIELD, ElementType.TYPE_USE})
 | 
			
		||||
@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
@Documented
 | 
			
		||||
public @interface AccessFor {
 | 
			
		||||
    Role[] init() default Role.NOBODY;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,13 @@ import com.fasterxml.jackson.databind.node.IntNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.LongNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.TextNode;
 | 
			
		||||
import org.apache.commons.lang3.NotImplementedException;
 | 
			
		||||
import org.hostsharing.hsadminng.security.SecurityUtils;
 | 
			
		||||
import org.hostsharing.hsadminng.service.util.ReflectionUtil;
 | 
			
		||||
import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException;
 | 
			
		||||
 | 
			
		||||
import java.lang.reflect.Field;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked;
 | 
			
		||||
 | 
			
		||||
@@ -17,6 +21,8 @@ public class JSonDeserializerWithAccessFilter<T> {
 | 
			
		||||
 | 
			
		||||
    private final T dto;
 | 
			
		||||
    private final TreeNode treeNode;
 | 
			
		||||
    private final Set<Field> modifiedFields = new HashSet<>();
 | 
			
		||||
    private Field selfIdField = null;
 | 
			
		||||
 | 
			
		||||
    public JSonDeserializerWithAccessFilter(final JsonParser jsonParser, final DeserializationContext deserializationContext, Class<T> dtoClass) {
 | 
			
		||||
        this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser));
 | 
			
		||||
@@ -30,35 +36,82 @@ public class JSonDeserializerWithAccessFilter<T> {
 | 
			
		||||
                final Field field = dto.getClass().getDeclaredField(fieldName);
 | 
			
		||||
                final Object value = readValue(treeNode, field);
 | 
			
		||||
                writeValue(dto, field, value);
 | 
			
		||||
                markAsModified(field);
 | 
			
		||||
            } catch (NoSuchFieldException e) {
 | 
			
		||||
                throw new RuntimeException("setting field " + fieldName + " failed", e);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        modifiedFields.forEach(this::checkAccess);
 | 
			
		||||
        return dto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Object readValue(final TreeNode treeNode, final Field field) {
 | 
			
		||||
        final TreeNode fieldNode = treeNode.get(field.getName());
 | 
			
		||||
        if (fieldNode instanceof TextNode) {
 | 
			
		||||
            return ((TextNode)fieldNode).asText();
 | 
			
		||||
            return ((TextNode) fieldNode).asText();
 | 
			
		||||
        } else if (fieldNode instanceof IntNode) {
 | 
			
		||||
            return ((IntNode)fieldNode).asInt();
 | 
			
		||||
            return ((IntNode) fieldNode).asInt();
 | 
			
		||||
        } else if (fieldNode instanceof LongNode) {
 | 
			
		||||
            return ((LongNode)fieldNode).asLong();
 | 
			
		||||
            return ((LongNode) fieldNode).asLong();
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new NotImplementedException("property type not yet implemented: " + field);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void writeValue(final T dto, final Field field, final Object value) {
 | 
			
		||||
        if ( field.getType().isAssignableFrom(value.getClass()) ) {
 | 
			
		||||
        if (field.getType().isAssignableFrom(value.getClass())) {
 | 
			
		||||
            ReflectionUtil.setValue(dto, field, value);
 | 
			
		||||
        } else  if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) {
 | 
			
		||||
            ReflectionUtil.setValue(dto, field, ((Number)value).intValue());
 | 
			
		||||
        } else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) {
 | 
			
		||||
            ReflectionUtil.setValue(dto, field, ((Number) value).intValue());
 | 
			
		||||
        } else if (Long.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) {
 | 
			
		||||
            ReflectionUtil.setValue(dto, field, ((Number)value).longValue());
 | 
			
		||||
        } else  {
 | 
			
		||||
            ReflectionUtil.setValue(dto, field, ((Number) value).longValue());
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new NotImplementedException("property type not yet implemented: " + field);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void markAsModified(final Field field) {
 | 
			
		||||
        modifiedFields.add(field);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Object getId() {
 | 
			
		||||
        if (selfIdField == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return ReflectionUtil.getValue(dto, selfIdField);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void checkAccess(final Field field) {
 | 
			
		||||
        if ( !rememberSelfIdField(field) ) {
 | 
			
		||||
            if (getId() == null) {
 | 
			
		||||
                if (!getLoginUserRole().isAllowedToInit(field)) {
 | 
			
		||||
                    throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited");
 | 
			
		||||
                }
 | 
			
		||||
            } else if (getId() != null) {
 | 
			
		||||
                if (!getLoginUserRole().isAllowedToUpdate(field)) {
 | 
			
		||||
                    throw new BadRequestAlertException("Update of field prohibited for current user", toDisplay(field), "updateProhibited");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean rememberSelfIdField(final Field field) {
 | 
			
		||||
        if ( field.isAnnotationPresent(SelfId.class) ) {
 | 
			
		||||
            if ( selfIdField != null ) {
 | 
			
		||||
                throw new AssertionError("multiple " + SelfId.class + " detected in " + field.getDeclaringClass().getSimpleName() );
 | 
			
		||||
            }
 | 
			
		||||
            selfIdField = field;
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String toDisplay(final Field field) {
 | 
			
		||||
        return field.getDeclaringClass().getSimpleName() + "." + field.getName();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Role getLoginUserRole() {
 | 
			
		||||
        return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
package org.hostsharing.hsadminng.service.accessfilter;
 | 
			
		||||
 | 
			
		||||
import java.lang.annotation.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used to mark a field within a DTO as containing the id of a field,
 | 
			
		||||
 * it's needed to identify an existing entity for update functions.
 | 
			
		||||
 * Initialization and update rights have no meaning for such fields,
 | 
			
		||||
 * its initialized automatically and never updated.
 | 
			
		||||
 *
 | 
			
		||||
 * @see AccessFor
 | 
			
		||||
 */
 | 
			
		||||
@Documented
 | 
			
		||||
@Target({ElementType.FIELD})
 | 
			
		||||
@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
public @interface SelfId {
 | 
			
		||||
}
 | 
			
		||||
@@ -2,17 +2,11 @@ package org.hostsharing.hsadminng.service.dto;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.core.JsonGenerator;
 | 
			
		||||
import com.fasterxml.jackson.core.JsonParser;
 | 
			
		||||
import com.fasterxml.jackson.core.TreeNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.DeserializationContext;
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonDeserializer;
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonSerializer;
 | 
			
		||||
import com.fasterxml.jackson.databind.SerializerProvider;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.IntNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.TextNode;
 | 
			
		||||
import org.hostsharing.hsadminng.service.accessfilter.AccessFor;
 | 
			
		||||
import org.hostsharing.hsadminng.service.accessfilter.JSonDeserializerWithAccessFilter;
 | 
			
		||||
import org.hostsharing.hsadminng.service.accessfilter.JSonSerializerWithAccessFilter;
 | 
			
		||||
import org.hostsharing.hsadminng.service.accessfilter.Role;
 | 
			
		||||
import org.hostsharing.hsadminng.service.accessfilter.*;
 | 
			
		||||
import org.springframework.boot.jackson.JsonComponent;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.*;
 | 
			
		||||
@@ -25,6 +19,7 @@ import java.util.Objects;
 | 
			
		||||
 */
 | 
			
		||||
public class CustomerDTO implements Serializable {
 | 
			
		||||
 | 
			
		||||
    @SelfId
 | 
			
		||||
    @AccessFor(read = Role.ANY_CUSTOMER_USER)
 | 
			
		||||
    private Long id;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,15 @@ public class ReflectionUtil {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <T> Object getValue(T dto, Field field) {
 | 
			
		||||
        try {
 | 
			
		||||
            field.setAccessible(true);
 | 
			
		||||
            return field.get(dto);
 | 
			
		||||
        } catch (IllegalAccessException e) {
 | 
			
		||||
            throw new RuntimeException(e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @FunctionalInterface
 | 
			
		||||
    public interface ThrowingSupplier<T> {
 | 
			
		||||
        T get() throws Exception;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,32 +11,32 @@ public class BadRequestAlertException extends AbstractThrowableProblem {
 | 
			
		||||
 | 
			
		||||
    private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
    private final String entityName;
 | 
			
		||||
    private final String param;
 | 
			
		||||
 | 
			
		||||
    private final String errorKey;
 | 
			
		||||
 | 
			
		||||
    public BadRequestAlertException(String defaultMessage, String entityName, String errorKey) {
 | 
			
		||||
        this(ErrorConstants.DEFAULT_TYPE, defaultMessage, entityName, errorKey);
 | 
			
		||||
    public BadRequestAlertException(String defaultMessage, String param, String errorKey) {
 | 
			
		||||
        this(ErrorConstants.DEFAULT_TYPE, defaultMessage, param, errorKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public BadRequestAlertException(URI type, String defaultMessage, String entityName, String errorKey) {
 | 
			
		||||
        super(type, defaultMessage, Status.BAD_REQUEST, null, null, null, getAlertParameters(entityName, errorKey));
 | 
			
		||||
        this.entityName = entityName;
 | 
			
		||||
    public BadRequestAlertException(URI type, String defaultMessage, String param, String errorKey) {
 | 
			
		||||
        super(type, defaultMessage, Status.BAD_REQUEST, null, null, null, getAlertParameters(param, errorKey));
 | 
			
		||||
        this.param = param;
 | 
			
		||||
        this.errorKey = errorKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getEntityName() {
 | 
			
		||||
        return entityName;
 | 
			
		||||
    public String getParam() {
 | 
			
		||||
        return param;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getErrorKey() {
 | 
			
		||||
        return errorKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Map<String, Object> getAlertParameters(String entityName, String errorKey) {
 | 
			
		||||
    private static Map<String, Object> getAlertParameters(String param, String errorKey) {
 | 
			
		||||
        Map<String, Object> parameters = new HashMap<>();
 | 
			
		||||
        parameters.put("message", "error." + errorKey);
 | 
			
		||||
        parameters.put("params", entityName);
 | 
			
		||||
        parameters.put("params", param);
 | 
			
		||||
        return parameters;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -104,7 +104,7 @@ public class ExceptionTranslator implements ProblemHandling {
 | 
			
		||||
 | 
			
		||||
    @ExceptionHandler
 | 
			
		||||
    public ResponseEntity<Problem> handleBadRequestAlertException(BadRequestAlertException ex, NativeWebRequest request) {
 | 
			
		||||
        return create(ex, request, HeaderUtil.createFailureAlert(ex.getEntityName(), ex.getErrorKey(), ex.getMessage()));
 | 
			
		||||
        return create(ex, request, HeaderUtil.createFailureAlert(ex.getParam(), ex.getErrorKey(), ex.getMessage()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ExceptionHandler
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
        "shareTransactionImmutable": "Transaktionen mit Geschäftsanteilen sind unveränderlich",
 | 
			
		||||
        "membershipNotDeletable": "Mitgliedschaft kann nicht gelöscht werden, setze stattdessen das 'untilDate'",
 | 
			
		||||
        "untilDateMustBeAfterSinceDate": "Mitgliedshafts-Austrittsdatum muss nach dem Beitrittsdatum liegen",
 | 
			
		||||
        "anotherUncancelledMembershipExists": "Nur eine einzige ungekündigte Mitgliedschaft pro Kunde ist zulässig"
 | 
			
		||||
        "anotherUncancelledMembershipExists": "Nur eine einzige ungekündigte Mitgliedschaft pro Kunde ist zulässig",
 | 
			
		||||
        "initializationProhibited": "Initialisierung des Feldes unzulässig",
 | 
			
		||||
        "updateProhibited": "Aktualisierung des Feldes unzulässig"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
        "shareTransactionImmutable": "Share transactions are immutable",
 | 
			
		||||
        "membershipNotDeletable": "Membership cannot be deleted, instead set 'untilDate'",
 | 
			
		||||
        "untilDateMustBeAfterSinceDate": "Membership until date must be after since date",
 | 
			
		||||
        "anotherUncancelledMembershipExists": "Only a single uncancelled membership allowed per customer"
 | 
			
		||||
        "anotherUncancelledMembershipExists": "Only a single uncancelled membership allowed per customer",
 | 
			
		||||
        "initializationProhibited": "Initialization of the field prohibited",
 | 
			
		||||
        "updateProhibited": "Update of the field prohibited"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user