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