JSonDeserializerWithAccessFilter with working access rights validation
This commit is contained in:
parent
bb0fb4aa78
commit
63bd602397
@ -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());
|
||||
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());
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
package org.hostsharing.hsadminng.service.accessfilter;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeId;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.ObjectCodec;
|
||||
import com.fasterxml.jackson.core.TreeNode;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.hostsharing.hsadminng.service.dto.CustomerDTO;
|
||||
import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@ -22,9 +20,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class JSonDeserializerWithAccessFilterUnitTest {
|
||||
|
||||
@Rule
|
||||
@ -40,7 +37,7 @@ public class JSonDeserializerWithAccessFilterUnitTest {
|
||||
public TreeNode treeNode;
|
||||
|
||||
@Before
|
||||
public void init() throws IOException {
|
||||
public void init() {
|
||||
givenLoginUserWithRole(Role.ANY_CUSTOMER_USER);
|
||||
|
||||
given(jsonParser.getCodec()).willReturn(codec);
|
||||
@ -82,21 +79,81 @@ public class JSonDeserializerWithAccessFilterUnitTest {
|
||||
assertThat(actualDto.openLongField).isEqualTo(1234L);
|
||||
}
|
||||
|
||||
// --- fixture code below ---
|
||||
@Test
|
||||
public void shouldDeserializeStringFieldIfRequiredRoleIsCoveredByUser() throws IOException {
|
||||
// given
|
||||
givenLoginUserWithRole(Role.FINANCIAL_CONTACT);
|
||||
givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value")));
|
||||
|
||||
private String asJSon(final ImmutablePair<String, ? extends Object>... properties) {
|
||||
// when
|
||||
GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize();
|
||||
|
||||
// then
|
||||
assertThat(actualDto.restrictedField).isEqualTo("Restricted String Value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldInitializeFieldIfRequiredRoleIsNotCoveredByUser() throws IOException {
|
||||
// given
|
||||
givenLoginUserWithRole(Role.ANY_CUSTOMER_USER);
|
||||
givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value")));
|
||||
|
||||
// when
|
||||
Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize());
|
||||
|
||||
// then
|
||||
assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> {
|
||||
assertThat(badRequestAlertException.getParam()).isEqualTo("GivenDto.restrictedField");
|
||||
assertThat(badRequestAlertException.getErrorKey()).isEqualTo("initializationProhibited");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUpdateFieldIfRequiredRoleIsNotCoveredByUser() throws IOException {
|
||||
// given
|
||||
givenLoginUserWithRole(Role.ANY_CUSTOMER_USER);
|
||||
givenJSonTree(asJSon(
|
||||
ImmutablePair.of("id", 1234L),
|
||||
ImmutablePair.of("restrictedField", "Restricted String Value")));
|
||||
|
||||
// when
|
||||
Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize());
|
||||
|
||||
// then
|
||||
assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> {
|
||||
assertThat(badRequestAlertException.getParam()).isEqualTo("GivenDto.restrictedField");
|
||||
assertThat(badRequestAlertException.getErrorKey()).isEqualTo("updateProhibited");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void should() throws IOException {
|
||||
// given
|
||||
givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value")));
|
||||
|
||||
// when
|
||||
Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDtoWithMultipleSelfId.class).deserialize());
|
||||
|
||||
// then
|
||||
assertThat(exception).isInstanceOf(AssertionError.class).hasMessageContaining("xx");
|
||||
}
|
||||
|
||||
// --- only fixture code below ---
|
||||
|
||||
@SafeVarargs
|
||||
private final String asJSon(final ImmutablePair<String, Object>... properties) {
|
||||
final StringBuilder json = new StringBuilder();
|
||||
for ( ImmutablePair<String, ? extends Object> prop: properties ) {
|
||||
for (ImmutablePair<String, Object> prop : properties) {
|
||||
json.append(inQuotes(prop.left));
|
||||
json.append(": ");
|
||||
if ( prop.right instanceof Number ) {
|
||||
if (prop.right instanceof Number) {
|
||||
json.append(prop.right);
|
||||
} else {
|
||||
json.append(inQuotes(prop.right));
|
||||
}
|
||||
json.append(",\n");
|
||||
}
|
||||
return "{\n" + json.substring(0, json.length()-2) + "\n}";
|
||||
return "{\n" + json.substring(0, json.length() - 2) + "\n}";
|
||||
}
|
||||
|
||||
private void givenJSonTree(String givenJSon) throws IOException {
|
||||
@ -108,16 +165,34 @@ public class JSonDeserializerWithAccessFilterUnitTest {
|
||||
}
|
||||
|
||||
public static class GivenDto {
|
||||
@AccessFor(update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT})
|
||||
|
||||
@SelfId
|
||||
@AccessFor(read = Role.ANY_CUSTOMER_USER)
|
||||
Long id;
|
||||
|
||||
@AccessFor(init = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}, update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT})
|
||||
String restrictedField;
|
||||
|
||||
@AccessFor(update = Role.ANYBODY)
|
||||
@AccessFor(init = Role.ANYBODY, update = Role.ANYBODY)
|
||||
String openStringField;
|
||||
|
||||
@AccessFor(update = Role.ANYBODY)
|
||||
@AccessFor(init = Role.ANYBODY, update = Role.ANYBODY)
|
||||
Integer openIntegerField;
|
||||
|
||||
@AccessFor(update = Role.ANYBODY)
|
||||
@AccessFor(init = Role.ANYBODY, update = Role.ANYBODY)
|
||||
Long openLongField;
|
||||
}
|
||||
|
||||
|
||||
public static class GivenDtoWithMultipleSelfId {
|
||||
|
||||
@SelfId
|
||||
@AccessFor(read = Role.ANY_CUSTOMER_USER)
|
||||
Long id;
|
||||
|
||||
@SelfId
|
||||
@AccessFor(read = Role.ANY_CUSTOMER_USER)
|
||||
Long id2;
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user