1
0

JSonDeserializerWithAccessFilter with working access rights validation

This commit is contained in:
Michael Hoennig
2019-04-23 08:31:26 +02:00
parent bb0fb4aa78
commit 63bd602397
10 changed files with 198 additions and 45 deletions

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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"
}
}