1
0

JSonSerializer/DeserializerWithAccessFilter: also use role in parent

This commit is contained in:
Michael Hoennig
2019-04-23 14:37:49 +02:00
parent 1505e7bd66
commit a94516b3ce
12 changed files with 247 additions and 95 deletions

View File

@ -1,9 +1,12 @@
package org.hostsharing.hsadminng.security;
import org.hostsharing.hsadminng.service.accessfilter.Role;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
@ -11,6 +14,8 @@ import java.util.Optional;
*/
public final class SecurityUtils {
private static List<UserRoleAssignment> userRoleAssignments = new ArrayList<>();
private SecurityUtils() {
}
@ -73,4 +78,42 @@ public final class SecurityUtils {
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(authority)))
.orElse(false);
}
public static Role getLoginUserRoleFor(final Class<?> onDtoClass, final Long onId) {
final Role highestRole = userRoleAssignments.stream().
map(ura ->
matches(onDtoClass, onId, ura)
? ura.role
: Role.ANYBODY).
reduce(Role.ANYBODY, (r1, r2) -> r1.covers(r2) ? r1 : r2);
return highestRole;
}
private static boolean matches(Class<?> onDtoClass, Long onId, UserRoleAssignment ura) {
final boolean matches = (ura.onClass == null || onDtoClass == ura.onClass) && (ura.onId == null || onId.equals(ura.onId) );
return matches;
}
// TODO: depends on https://plan.hostsharing.net/project/hsadmin/us/67?milestone=34
public static void addUserRole(final Class<?> onClass, final Long onId, final Role role) {
userRoleAssignments.add(new UserRoleAssignment(onClass, onId, role));
}
public static void clearUserRoles() {
userRoleAssignments.clear();
}
private static class UserRoleAssignment {
final Class<?> onClass;
final Long onId;
final Role role;
UserRoleAssignment(Class<?> onClass, Long onId, Role role) {
this.onClass = onClass;
this.onId = onId;
this.role = role;
}
}
}

View File

@ -0,0 +1,65 @@
package org.hostsharing.hsadminng.service.accessfilter;
import org.hostsharing.hsadminng.security.SecurityUtils;
import org.hostsharing.hsadminng.service.util.ReflectionUtil;
import java.lang.reflect.Field;
abstract class JSonAccessFilter<T> {
final T dto;
Field selfIdField = null;
Field parentIdField = null;
JSonAccessFilter(final T dto) {
this.dto = dto;
determineIdFields();
}
void determineIdFields() {
for (Field field : dto.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(SelfId.class)) {
if (selfIdField != null) {
throw new AssertionError("multiple @" + SelfId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName());
}
selfIdField = field;
}
if (field.isAnnotationPresent(ParentId.class)) {
if (parentIdField != null) {
throw new AssertionError("multiple @" + ParentId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName());
}
parentIdField = field;
}
}
}
Long getId() {
if (selfIdField == null) {
return null;
}
return (Long) ReflectionUtil.getValue(dto, selfIdField);
}
String toDisplay(final Field field) {
return field.getDeclaringClass().getSimpleName() + "." + field.getName();
}
Role getLoginUserRole() {
final Role roleOnSelf = getLoginUserRoleOnSelf();
final Role roleOnParent = getLoginUserRoleOnParent();
return roleOnSelf.covers(roleOnParent) ? roleOnSelf : roleOnParent;
}
private Role getLoginUserRoleOnSelf() {
// TODO: find broadest role in self and recursively in parent
return SecurityUtils.getLoginUserRoleFor(dto.getClass(), getId() );
}
private Role getLoginUserRoleOnParent() {
if ( parentIdField == null ) {
return Role.ANYBODY;
}
final ParentId parentId = parentIdField.getAnnotation(ParentId.class);
return SecurityUtils.getLoginUserRoleFor(parentId.value(), (Long) ReflectionUtil.getValue(dto, parentIdField) );
}
}

View File

@ -7,7 +7,6 @@ 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;
@ -17,37 +16,23 @@ import java.util.Set;
import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked;
public class JSonDeserializerWithAccessFilter<T> {
public class JSonDeserializerWithAccessFilter<T> extends JSonAccessFilter<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) {
super(unchecked(dtoClass::newInstance));
this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser));
this.dto = unchecked(dtoClass::newInstance);
}
// Jackson deserializes from the JsonParser, thus no input parameter needed.
public T deserialize() {
determineSelfIdField();
deserializeValues();
checkAccessToModifiedFields();
return dto;
}
private void determineSelfIdField() {
for (Field field : dto.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(SelfId.class)) {
if (selfIdField != null) {
throw new AssertionError("multiple @" + SelfId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName());
}
selfIdField = field;
}
}
}
private void deserializeValues() {
treeNode.fieldNames().forEachRemaining(fieldName -> {
try {
@ -90,34 +75,21 @@ public class JSonDeserializerWithAccessFilter<T> {
modifiedFields.add(field);
}
private Object getId() {
if (selfIdField == null) {
return null;
}
return ReflectionUtil.getValue(dto, selfIdField);
}
private void checkAccessToModifiedFields() {
modifiedFields.forEach(field -> {
if ( !field.equals(selfIdField) ) {
if (getId() == null) {
if (!getLoginUserRole().isAllowedToInit(field)) {
throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited");
if ( !field.equals(parentIdField)) {
throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited");
} else {
throw new BadRequestAlertException("Referencing field prohibited for current user", toDisplay(field), "referencingProhibited");
}
}
} else if (getId() != null) {
if (!getLoginUserRole().isAllowedToUpdate(field)) {
} else if (!getLoginUserRole().isAllowedToUpdate(field)) {
throw new BadRequestAlertException("Update of field prohibited for current user", toDisplay(field), "updateProhibited");
}
}
}
});
}
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

@ -2,28 +2,22 @@ package org.hostsharing.hsadminng.service.accessfilter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.apache.commons.lang3.NotImplementedException;
import org.hostsharing.hsadminng.security.SecurityUtils;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class JSonSerializerWithAccessFilter <T> {
public class JSonSerializerWithAccessFilter <T> extends JSonAccessFilter<T> {
private final JsonGenerator jsonGenerator;
private final SerializerProvider serializerProvider;
private final T dto;
public JSonSerializerWithAccessFilter(final JsonGenerator jsonGenerator,
final SerializerProvider serializerProvider,
final T dto) {
super(dto);
this.jsonGenerator = jsonGenerator;
this.serializerProvider = serializerProvider;
this.dto = dto;
}
// Jackson serializes into the JsonGenerator, thus no return value needed.
@ -65,7 +59,4 @@ public class JSonSerializerWithAccessFilter <T> {
}
}
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 referenced entity,
* it's needed to determine access rights for entity creation.
*
* @see AccessFor
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ParentId {
/// The DTO class of the referenced entity.
Class<?> value();
}

View File

@ -1,7 +1,5 @@
package org.hostsharing.hsadminng.service.accessfilter;
import org.hostsharing.hsadminng.security.SecurityUtils;
import java.lang.reflect.Field;
/**
@ -52,7 +50,7 @@ public enum Role {
*/
FINANCIAL_CONTACT(22) {
@Override
boolean covers(final Role role) {
public boolean covers(final Role role) {
if (role == ACTUAL_CUSTOMER_USER) {
return false;
}
@ -94,14 +92,15 @@ public enum Role {
*
* Where 'this' means the Java instance itself as a role of a system user.
*
* @example
* {@code
* Role.HOSTMASTER.covers(Role.ANY_CUSTOMER_USER) == true
* }
*
* @param role The required role for a resource.
*
* @return whether this role comprises the given role
*/
boolean covers(final Role role) {
public boolean covers(final Role role) {
return this == role || this.level < role.level;
}
@ -131,8 +130,6 @@ public enum Role {
*/
public boolean isAllowedToUpdate(final Field field) {
final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY);
final AccessFor accessFor = field.getAnnotation(AccessFor.class);
if (accessFor == null) {
return false;
@ -150,8 +147,6 @@ public enum Role {
*/
public boolean isAllowedToRead(final Field field) {
final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY);
final AccessFor accessFor = field.getAnnotation(AccessFor.class);
if (accessFor == null) {
return false;

View File

@ -1,7 +1,9 @@
package org.hostsharing.hsadminng.service.dto;
import org.hostsharing.hsadminng.service.accessfilter.AccessFor;
import org.hostsharing.hsadminng.service.accessfilter.ParentId;
import org.hostsharing.hsadminng.service.accessfilter.Role;
import org.hostsharing.hsadminng.service.accessfilter.SelfId;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@ -15,6 +17,7 @@ import java.util.function.Consumer;
*/
public class MembershipDTO implements Serializable {
@SelfId
@AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT})
private Long id;
@ -33,8 +36,8 @@ public class MembershipDTO implements Serializable {
@AccessFor(init = Role.ADMIN, read = Role.SUPPORTER)
private String remark;
// TODO @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER)
// @AccessReference(CustomerDTO.class, Role...)
@ParentId(CustomerDTO.class)
@AccessFor(init = Role.ADMIN, read = Role.SUPPORTER)
private Long customerId;
@AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT})

View File

@ -1,9 +1,6 @@
package org.hostsharing.hsadminng.service.util;
import com.fasterxml.jackson.core.TreeNode;
import java.lang.reflect.Field;
import java.util.function.Supplier;
public class ReflectionUtil {
@ -26,10 +23,10 @@ public class ReflectionUtil {
}
}
public static <T> Object getValue(T dto, Field field) {
public static <T> T getValue(final T dto, final Field field) {
try {
field.setAccessible(true);
return field.get(dto);
return (T) field.get(dto);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}