#146 [AccessRights] simple AccessFilter for UserRoleAssignments
allows r/w for ADMINs and r/o for SUPPORTERs no entity dependent access rights implemented yet
This commit is contained in:
parent
3143f27b6c
commit
60612f6c41
@ -2,6 +2,7 @@
|
||||
package org.hostsharing.hsadminng.domain;
|
||||
|
||||
import org.hostsharing.hsadminng.config.Constants;
|
||||
import org.hostsharing.hsadminng.service.dto.FluentBuilder;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
@ -27,7 +28,7 @@ import javax.validation.constraints.Size;
|
||||
@Entity
|
||||
@Table(name = "jhi_user")
|
||||
|
||||
public class User extends AbstractAuditingEntity implements Serializable {
|
||||
public class User extends AbstractAuditingEntity implements FluentBuilder<User>, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ -92,7 +93,6 @@ public class User extends AbstractAuditingEntity implements Serializable {
|
||||
name = "jhi_user_authority",
|
||||
joinColumns = { @JoinColumn(name = "user_id", referencedColumnName = "id") },
|
||||
inverseJoinColumns = { @JoinColumn(name = "authority_name", referencedColumnName = "name") })
|
||||
|
||||
@BatchSize(size = 20)
|
||||
private Set<Authority> authorities = new HashSet<>();
|
||||
|
||||
@ -100,6 +100,11 @@ public class User extends AbstractAuditingEntity implements Serializable {
|
||||
return id;
|
||||
}
|
||||
|
||||
public User id(final long id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
// Licensed under Apache-2.0
|
||||
package org.hostsharing.hsadminng.domain;
|
||||
|
||||
import org.hostsharing.hsadminng.service.accessfilter.Role;
|
||||
import org.hostsharing.hsadminng.repository.UserRepository;
|
||||
import org.hostsharing.hsadminng.service.UserRoleAssignmentService;
|
||||
import org.hostsharing.hsadminng.service.accessfilter.*;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.TreeNode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import org.springframework.boot.jackson.JsonComponent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.persistence.*;
|
||||
@ -16,31 +23,42 @@ import javax.validation.constraints.*;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "user_role_assignment")
|
||||
public class UserRoleAssignment implements Serializable {
|
||||
@EntityTypeId(UserRoleAssignment.ENTITY_TYPE_ID)
|
||||
public class UserRoleAssignment implements AccessMappings {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final String ENTITY_TYPE_ID = "rights.UserRoleAssignment";
|
||||
|
||||
static final String USER_FIELD_NAME = "user";
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
|
||||
@SequenceGenerator(name = "sequenceGenerator")
|
||||
@SelfId(resolver = UserRoleAssignmentService.class)
|
||||
@AccessFor(read = Role.SUPPORTER)
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@Size(max = 32)
|
||||
@Column(name = "entity_type_id", length = 32, nullable = false)
|
||||
@AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.SUPPORTER)
|
||||
private String entityTypeId;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "entity_object_id", nullable = false)
|
||||
@AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.SUPPORTER)
|
||||
private Long entityObjectId;
|
||||
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "assigned_role", nullable = false)
|
||||
@AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.SUPPORTER)
|
||||
private Role assignedRole;
|
||||
|
||||
@ManyToOne
|
||||
@JsonIgnoreProperties("requireds")
|
||||
@AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.SUPPORTER)
|
||||
private User user;
|
||||
|
||||
// jhipster-needle-entity-add-field - JHipster will add fields here, do not remove
|
||||
@ -49,6 +67,11 @@ public class UserRoleAssignment implements Serializable {
|
||||
return id;
|
||||
}
|
||||
|
||||
public UserRoleAssignment id(final long id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
@ -136,4 +159,49 @@ public class UserRoleAssignment implements Serializable {
|
||||
", assignedRole='" + getAssignedRole() + "'" +
|
||||
"}";
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public static class UserRoleAssignmentJsonSerializer extends JsonSerializerWithAccessFilter<UserRoleAssignment> {
|
||||
|
||||
public UserRoleAssignmentJsonSerializer(
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService) {
|
||||
super(ctx, userRoleAssignmentService);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JSonFieldWriter<UserRoleAssignment> jsonFieldWriter(final Field field) {
|
||||
if (USER_FIELD_NAME.equals(field.getName())) {
|
||||
return (final UserRoleAssignment dto, final JsonGenerator jsonGenerator) -> {
|
||||
jsonGenerator.writeNumberField(USER_FIELD_NAME, dto.getUser().getId());
|
||||
};
|
||||
}
|
||||
return super.jsonFieldWriter(field);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public static class UserRoleAssignmentJsonDeserializer extends JsonDeserializerWithAccessFilter<UserRoleAssignment> {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserRoleAssignmentJsonDeserializer(
|
||||
final UserRepository userRepository,
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService) {
|
||||
super(ctx, userRoleAssignmentService);
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JSonFieldReader<UserRoleAssignment> jsonFieldReader(final TreeNode treeNode, final Field field) {
|
||||
if ("user".equals(field.getName())) {
|
||||
return (final UserRoleAssignment target) -> {
|
||||
target.setUser(userRepository.getOne(getSubNode(treeNode, "id")));
|
||||
};
|
||||
}
|
||||
|
||||
return super.jsonFieldReader(treeNode, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class UserRoleAssignmentService {
|
||||
public class UserRoleAssignmentService implements IdToDtoResolver<UserRoleAssignment> {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(UserRoleAssignmentService.class);
|
||||
|
||||
|
@ -22,7 +22,7 @@ import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
abstract class JSonAccessFilter<T> {
|
||||
abstract class JSonAccessFilter<T extends AccessMappings> {
|
||||
|
||||
private final ApplicationContext ctx;
|
||||
private final UserRoleAssignmentService userRoleAssignmentService;
|
||||
|
@ -1,211 +0,0 @@
|
||||
// Licensed under Apache-2.0
|
||||
package org.hostsharing.hsadminng.service.accessfilter;
|
||||
|
||||
import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked;
|
||||
|
||||
import org.hostsharing.hsadminng.service.UserRoleAssignmentService;
|
||||
import org.hostsharing.hsadminng.service.util.ReflectionUtil;
|
||||
import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.TreeNode;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.node.*;
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Actual implementation of JSON deserialization, where {link JSonDeserializerWithAccessFilter}
|
||||
* is a stateless bean, {@link JSonDeserializationWithAccessFilter} exists only during the actual
|
||||
* deserialization and contains a deserialization state.
|
||||
*
|
||||
* @param <T> DTO class to serialize
|
||||
*/
|
||||
public class JSonDeserializationWithAccessFilter<T> extends JSonAccessFilter<T> {
|
||||
|
||||
private final TreeNode treeNode;
|
||||
private final Set<Field> updatingFields = new HashSet<>();
|
||||
|
||||
public JSonDeserializationWithAccessFilter(
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService,
|
||||
final JsonParser jsonParser,
|
||||
final DeserializationContext deserializationContext,
|
||||
Class<T> dtoClass) {
|
||||
super(ctx, userRoleAssignmentService, unchecked(dtoClass::newInstance));
|
||||
this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser));
|
||||
}
|
||||
|
||||
// Jackson deserializes from the JsonParser, thus no input parameter needed.
|
||||
public T deserialize() {
|
||||
deserializeValues();
|
||||
final T currentDto = loadCurrentDto(getId());
|
||||
overwriteUnmodifiedFieldsWithCurrentValues(currentDto);
|
||||
checkAccessToWrittenFields(currentDto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void deserializeValues() {
|
||||
treeNode.fieldNames().forEachRemaining(fieldName -> {
|
||||
try {
|
||||
final Field field = dto.getClass().getDeclaredField(fieldName);
|
||||
final Object newValue = readValueFromJSon(treeNode, field);
|
||||
writeValueToDto(dto, field, newValue);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException("setting field " + fieldName + " failed", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private T loadCurrentDto(final Long id) {
|
||||
if (id != null) {
|
||||
return (T) loadDto(selfIdField.getAnnotation(SelfId.class).resolver(), id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void overwriteUnmodifiedFieldsWithCurrentValues(final T currentDto) {
|
||||
if (currentDto == null) {
|
||||
return;
|
||||
}
|
||||
for (Field field : currentDto.getClass().getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(AccessFor.class)) {
|
||||
boolean updatingField = updatingFields.contains(field);
|
||||
if (updatingField && !isActuallyUpdated(field, dto, currentDto)) {
|
||||
updatingFields.remove(field);
|
||||
updatingField = false;
|
||||
}
|
||||
if (!updatingField) {
|
||||
final Object value = ReflectionUtil.getValue(currentDto, field);
|
||||
ReflectionUtil.setValue(dto, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private Object readValueFromJSon(final TreeNode treeNode, final Field field) {
|
||||
return readValueFromJSon(treeNode, field.getName(), field.getType());
|
||||
}
|
||||
|
||||
private Object readValueFromJSon(final TreeNode treeNode, final String fieldName, final Class<?> fieldClass) {
|
||||
final TreeNode fieldNode = treeNode.get(fieldName);
|
||||
if (fieldNode instanceof NullNode) {
|
||||
return null;
|
||||
}
|
||||
if (fieldNode instanceof TextNode) {
|
||||
return ((TextNode) fieldNode).asText();
|
||||
}
|
||||
if (fieldNode instanceof IntNode) {
|
||||
return ((IntNode) fieldNode).asInt();
|
||||
}
|
||||
if (fieldNode instanceof LongNode) {
|
||||
return ((LongNode) fieldNode).asLong();
|
||||
}
|
||||
if (fieldNode instanceof DoubleNode) {
|
||||
// TODO: we need to figure out, why DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS does not work
|
||||
return ((DoubleNode) fieldNode).asDouble();
|
||||
}
|
||||
if (fieldNode instanceof ArrayNode && LocalDate.class.isAssignableFrom(fieldClass)) {
|
||||
return LocalDate.of(
|
||||
((ArrayNode) fieldNode).get(0).asInt(),
|
||||
((ArrayNode) fieldNode).get(1).asInt(),
|
||||
((ArrayNode) fieldNode).get(2).asInt());
|
||||
}
|
||||
throw new NotImplementedException(
|
||||
"JSon node type not implemented: " + fieldNode.getClass() + " -> " + fieldName + ": " + fieldClass);
|
||||
}
|
||||
|
||||
private void writeValueToDto(final T dto, final Field field, final Object value) {
|
||||
if (value == null) {
|
||||
ReflectionUtil.setValue(dto, field, null);
|
||||
} else if (field.getType().isAssignableFrom(value.getClass())) {
|
||||
ReflectionUtil.setValue(dto, field, value);
|
||||
} else if (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 if (BigDecimal.class.isAssignableFrom(field.getType())) {
|
||||
ReflectionUtil.setValue(dto, field, new BigDecimal(value.toString()));
|
||||
} else if (Boolean.class.isAssignableFrom(field.getType()) || boolean.class.isAssignableFrom(field.getType())) {
|
||||
ReflectionUtil.setValue(dto, field, Boolean.valueOf(value.toString()));
|
||||
} else if (field.getType().isEnum()) {
|
||||
ReflectionUtil.setValue(dto, field, ReflectionUtil.asEnumValue(field.getType(), value));
|
||||
} else if (LocalDate.class.isAssignableFrom(field.getType())) {
|
||||
ReflectionUtil.setValue(dto, field, LocalDate.parse(value.toString()));
|
||||
} else {
|
||||
throw new NotImplementedException("property type not yet implemented: " + field);
|
||||
}
|
||||
updatingFields.add(field);
|
||||
}
|
||||
|
||||
private void checkAccessToWrittenFields(final T currentDto) {
|
||||
updatingFields.forEach(
|
||||
field -> {
|
||||
// TODO this ugly code needs cleanup
|
||||
if (!field.equals(selfIdField)) {
|
||||
final Set<Role> roles = getLoginUserRoles();
|
||||
if (isInitAccess()) {
|
||||
if (!isAllowedToInit(roles, field)) {
|
||||
if (!field.equals(parentIdField)) {
|
||||
throw new BadRequestAlertException(
|
||||
"Initialization of field " + toDisplay(field)
|
||||
+ " prohibited for current user role(s): "
|
||||
+ Joiner.on("+").join(roles),
|
||||
toDisplay(field),
|
||||
"initializationProhibited");
|
||||
} else {
|
||||
throw new BadRequestAlertException(
|
||||
"Referencing field " + toDisplay(field) + " prohibited for current user role(s): "
|
||||
+ Joiner.on("+").join(roles),
|
||||
toDisplay(field),
|
||||
"referencingProhibited");
|
||||
}
|
||||
}
|
||||
} else if (!Role.toBeIgnoredForUpdates(field) && !isAllowedToUpdate(getLoginUserRoles(), field)) {
|
||||
throw new BadRequestAlertException(
|
||||
"Update of field " + toDisplay(field) + " prohibited for current user role(s): "
|
||||
+ Joiner.on("+").join(roles),
|
||||
toDisplay(field),
|
||||
"updateProhibited");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isAllowedToInit(final Set<Role> roles, final Field field) {
|
||||
for (Role role : roles) {
|
||||
if (role.isAllowedToInit(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAllowedToUpdate(final Set<Role> roles, final Field field) {
|
||||
for (Role role : roles) {
|
||||
if (role.isAllowedToUpdate(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isInitAccess() {
|
||||
return getId() == null;
|
||||
}
|
||||
|
||||
private boolean isActuallyUpdated(final Field field, final T dto, T currentDto) {
|
||||
return 0 != ObjectUtils.compare(ReflectionUtil.getValue(dto, field), ReflectionUtil.getValue(currentDto, field));
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Licensed under Apache-2.0
|
||||
package org.hostsharing.hsadminng.service.accessfilter;
|
||||
|
||||
/**
|
||||
* Reads a JSON node value.
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface JSonFieldReader<T extends AccessMappings> {
|
||||
|
||||
/**
|
||||
* Reads a JSON node value.
|
||||
*
|
||||
* @param target your target entity or DTO type
|
||||
*
|
||||
*/
|
||||
void readInto(T target);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Licensed under Apache-2.0
|
||||
package org.hostsharing.hsadminng.service.accessfilter;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Similar to a BiConsumer<T, JsonGenerator>, but declaring IOException as needed by JsonGenerator.
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface JSonFieldWriter<T extends AccessMappings> {
|
||||
|
||||
/**
|
||||
* Writes a JSON field and value.
|
||||
*
|
||||
* @param object your entity or DTO type
|
||||
* @param jsonGenerator provides low level methods for writing JSON fields
|
||||
*/
|
||||
void write(T object, JsonGenerator jsonGenerator) throws IOException;
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
// Licensed under Apache-2.0
|
||||
package org.hostsharing.hsadminng.service.accessfilter;
|
||||
|
||||
import org.hostsharing.hsadminng.service.UserRoleAssignmentService;
|
||||
import org.hostsharing.hsadminng.service.util.ReflectionUtil;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Actual implementation of JSON serialization, where {link JsonSerializerWithAccessFilter}
|
||||
* is a stateless bean, {@link JSonSerializationWithAccessFilter} exists only during the actual
|
||||
* serialization and contains a serialization state.
|
||||
*
|
||||
* @param <T> DTO class to serialize
|
||||
*/
|
||||
public class JSonSerializationWithAccessFilter<T> extends JSonAccessFilter<T> {
|
||||
|
||||
private final JsonGenerator jsonGenerator;
|
||||
private final SerializerProvider serializerProvider;
|
||||
|
||||
public JSonSerializationWithAccessFilter(
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService,
|
||||
final JsonGenerator jsonGenerator,
|
||||
final SerializerProvider serializerProvider,
|
||||
final T dto) {
|
||||
super(ctx, userRoleAssignmentService, dto);
|
||||
this.jsonGenerator = jsonGenerator;
|
||||
this.serializerProvider = serializerProvider;
|
||||
}
|
||||
|
||||
// Jackson serializes into the JsonGenerator, thus no return value needed.
|
||||
public void serialize() throws IOException {
|
||||
|
||||
jsonGenerator.writeStartObject();
|
||||
for (Field field : dto.getClass().getDeclaredFields()) {
|
||||
toJSon(dto, jsonGenerator, field);
|
||||
}
|
||||
jsonGenerator.writeEndObject();
|
||||
}
|
||||
|
||||
private void toJSon(final Object dto, final JsonGenerator jsonGenerator, final Field field) throws IOException {
|
||||
if (isAllowedToRead(getLoginUserRoles(), field)) {
|
||||
final String fieldName = field.getName();
|
||||
// TODO: maybe replace by serializerProvider.defaultSerialize...()?
|
||||
// But that makes it difficult for parallel structure with the deserializer (clumsy API).
|
||||
// Alternatively extract the supported types to subclasses of some abstract class and
|
||||
// here as well as in the deserializer just access the matching implementation through a map.
|
||||
// Or even completely switch from Jackson to GSON?
|
||||
final Object fieldValue = ReflectionUtil.getValue(dto, field);
|
||||
if (fieldValue == null) {
|
||||
jsonGenerator.writeNullField(fieldName);
|
||||
} else if (String.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeStringField(fieldName, (String) fieldValue);
|
||||
} else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeNumberField(fieldName, (int) fieldValue);
|
||||
} else if (Long.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeNumberField(fieldName, (long) fieldValue);
|
||||
} else if (LocalDate.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeStringField(fieldName, fieldValue.toString());
|
||||
} else if (Enum.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeStringField(fieldName, ((Enum) fieldValue).name());
|
||||
} else if (Boolean.class.isAssignableFrom(field.getType()) || boolean.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeBooleanField(fieldName, (Boolean) fieldValue);
|
||||
} else if (BigDecimal.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeNumberField(fieldName, (BigDecimal) fieldValue);
|
||||
} else {
|
||||
throw new NotImplementedException("property type not yet implemented: " + field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAllowedToRead(final Set<Role> roles, final Field field) {
|
||||
for (Role role : roles) {
|
||||
if (role.isAllowedToRead(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Role.ANYBODY.isAllowedToRead(field);
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +1,30 @@
|
||||
// Licensed under Apache-2.0
|
||||
package org.hostsharing.hsadminng.service.accessfilter;
|
||||
|
||||
import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked;
|
||||
|
||||
import org.hostsharing.hsadminng.service.UserRoleAssignmentService;
|
||||
import org.hostsharing.hsadminng.service.util.ReflectionUtil;
|
||||
import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException;
|
||||
|
||||
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.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.*;
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class JsonDeserializerWithAccessFilter<T extends AccessMappings> extends JsonDeserializer<T> {
|
||||
|
||||
private final ApplicationContext ctx;
|
||||
@ -29,11 +44,223 @@ public abstract class JsonDeserializerWithAccessFilter<T extends AccessMappings>
|
||||
|
||||
final Class<T> dtoClass = ReflectionUtil
|
||||
.determineGenericClassParameter(this.getClass(), JsonDeserializerWithAccessFilter.class, 0);
|
||||
return new JSonDeserializationWithAccessFilter<T>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
deserializationContext,
|
||||
dtoClass).deserialize();
|
||||
// @formatter:off
|
||||
return new JSonDeserializationWithAccessFilter(
|
||||
this, ctx, userRoleAssignmentService, jsonParser, deserializationContext, dtoClass)
|
||||
.deserialize();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
protected JSonFieldReader<T> jsonFieldReader(final TreeNode treeNode, final Field field) {
|
||||
return (final T object) -> {
|
||||
final Object newValue = readValueFromJSon(treeNode, field);
|
||||
writeValueToDto(object, field, newValue);
|
||||
};
|
||||
}
|
||||
|
||||
protected final Long getSubNode(final TreeNode node, final String name) {
|
||||
if (!node.isObject()) {
|
||||
throw new IllegalArgumentException(node + " is not a JSON object");
|
||||
}
|
||||
final ObjectNode objectNode = (ObjectNode) node;
|
||||
final JsonNode subNode = objectNode.get(name);
|
||||
if (!subNode.isNumber()) {
|
||||
throw new IllegalArgumentException(node + "." + name + " is not a number");
|
||||
}
|
||||
return subNode.asLong();
|
||||
}
|
||||
|
||||
private Object readValueFromJSon(final TreeNode treeNode, final Field field) {
|
||||
return readValueFromJSon(treeNode, field.getName(), field.getType());
|
||||
}
|
||||
|
||||
private Object readValueFromJSon(final TreeNode treeNode, final String fieldName, final Class<?> fieldClass) {
|
||||
// FIXME can be removed? final TreeNode fieldNode = treeNode.get(fieldName);
|
||||
final TreeNode fieldNode = treeNode;
|
||||
if (fieldNode instanceof NullNode) {
|
||||
return null;
|
||||
}
|
||||
if (fieldNode instanceof TextNode) {
|
||||
return ((TextNode) fieldNode).asText();
|
||||
}
|
||||
if (fieldNode instanceof IntNode) {
|
||||
return ((IntNode) fieldNode).asInt();
|
||||
}
|
||||
if (fieldNode instanceof LongNode) {
|
||||
return ((LongNode) fieldNode).asLong();
|
||||
}
|
||||
if (fieldNode instanceof DoubleNode) {
|
||||
// TODO: we need to figure out, why DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS does not work
|
||||
return ((DoubleNode) fieldNode).asDouble();
|
||||
}
|
||||
if (fieldNode instanceof ArrayNode && LocalDate.class.isAssignableFrom(fieldClass)) {
|
||||
return LocalDate.of(
|
||||
((ArrayNode) fieldNode).get(0).asInt(),
|
||||
((ArrayNode) fieldNode).get(1).asInt(),
|
||||
((ArrayNode) fieldNode).get(2).asInt());
|
||||
}
|
||||
throw new NotImplementedException(
|
||||
"JSon node type not implemented: " + fieldNode.getClass() + " -> " + fieldName + ": " + fieldClass);
|
||||
}
|
||||
|
||||
private void writeValueToDto(final T dto, final Field field, final Object value) {
|
||||
if (value == null) {
|
||||
ReflectionUtil.setValue(dto, field, null);
|
||||
} else if (field.getType().isAssignableFrom(value.getClass())) {
|
||||
ReflectionUtil.setValue(dto, field, value);
|
||||
} else if (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 if (BigDecimal.class.isAssignableFrom(field.getType())) {
|
||||
ReflectionUtil.setValue(dto, field, new BigDecimal(value.toString()));
|
||||
} else if (Boolean.class.isAssignableFrom(field.getType()) || boolean.class.isAssignableFrom(field.getType())) {
|
||||
ReflectionUtil.setValue(dto, field, Boolean.valueOf(value.toString()));
|
||||
} else if (field.getType().isEnum()) {
|
||||
ReflectionUtil.setValue(dto, field, ReflectionUtil.asEnumValue(field.getType(), value));
|
||||
} else if (LocalDate.class.isAssignableFrom(field.getType())) {
|
||||
ReflectionUtil.setValue(dto, field, LocalDate.parse(value.toString()));
|
||||
} else {
|
||||
throw new NotImplementedException("property type not yet implemented: " + field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of JSON deserialization, where {@link JsonDeserializerWithAccessFilter}
|
||||
* is a stateless bean, this inner class exists only during the actual deserialization and contains
|
||||
* the deserialization state.
|
||||
*/
|
||||
private class JSonDeserializationWithAccessFilter extends JSonAccessFilter<T> {
|
||||
|
||||
private final TreeNode treeNode;
|
||||
private final Set<Field> updatingFields = new HashSet<>();
|
||||
|
||||
public JSonDeserializationWithAccessFilter(
|
||||
final JsonDeserializerWithAccessFilter deserializer,
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService,
|
||||
final JsonParser jsonParser,
|
||||
final DeserializationContext deserializationContext,
|
||||
Class<T> dtoClass) {
|
||||
super(ctx, userRoleAssignmentService, unchecked(dtoClass::newInstance));
|
||||
this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser));
|
||||
}
|
||||
|
||||
// Jackson deserializes from the JsonParser, thus no input parameter needed.
|
||||
public T deserialize() {
|
||||
deserializeValues();
|
||||
final T currentDto = loadCurrentDto(getId());
|
||||
overwriteUnmodifiedFieldsWithCurrentValues(currentDto);
|
||||
checkAccessToWrittenFields(currentDto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void deserializeValues() {
|
||||
treeNode.fieldNames().forEachRemaining(fieldName -> {
|
||||
try {
|
||||
final Field field = dto.getClass().getDeclaredField(fieldName);
|
||||
final TreeNode node = treeNode.get(fieldName);
|
||||
jsonFieldReader(node, field).readInto(dto);
|
||||
updatingFields.add(field);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new RuntimeException("setting field " + fieldName + " failed", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private T loadCurrentDto(final Long id) {
|
||||
if (id != null) {
|
||||
return (T) loadDto(selfIdField.getAnnotation(SelfId.class).resolver(), id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void overwriteUnmodifiedFieldsWithCurrentValues(final T currentDto) {
|
||||
if (currentDto == null) {
|
||||
return;
|
||||
}
|
||||
for (Field field : currentDto.getClass().getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(AccessFor.class)) {
|
||||
boolean updatingField = updatingFields.contains(field);
|
||||
if (updatingField && !isActuallyUpdated(field, dto, currentDto)) {
|
||||
updatingFields.remove(field);
|
||||
updatingField = false;
|
||||
}
|
||||
if (!updatingField) {
|
||||
final Object value = ReflectionUtil.getValue(currentDto, field);
|
||||
ReflectionUtil.setValue(dto, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAccessToWrittenFields(final T currentDto) {
|
||||
updatingFields.forEach(
|
||||
field -> {
|
||||
// TODO this ugly code needs cleanup
|
||||
if (!field.equals(selfIdField)) {
|
||||
final Set<Role> roles = getLoginUserRoles();
|
||||
if (isInitAccess()) {
|
||||
if (!isAllowedToInit(roles, field)) {
|
||||
if (!field.equals(parentIdField)) {
|
||||
throw new BadRequestAlertException(
|
||||
"Initialization of field " + toDisplay(field)
|
||||
+ " prohibited for current user role(s): "
|
||||
+ Joiner.on("+").join(roles),
|
||||
toDisplay(field),
|
||||
"initializationProhibited");
|
||||
} else {
|
||||
throw new BadRequestAlertException(
|
||||
"Referencing field " + toDisplay(field)
|
||||
+ " prohibited for current user role(s): "
|
||||
+ Joiner.on("+").join(roles),
|
||||
toDisplay(field),
|
||||
"referencingProhibited");
|
||||
}
|
||||
}
|
||||
} else if (!Role.toBeIgnoredForUpdates(field) && !isAllowedToUpdate(getLoginUserRoles(), field)) {
|
||||
throw new BadRequestAlertException(
|
||||
"Update of field " + toDisplay(field) + " prohibited for current user role(s): "
|
||||
+ Joiner.on("+").join(roles),
|
||||
toDisplay(field),
|
||||
"updateProhibited");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isAllowedToInit(final Set<Role> roles, final Field field) {
|
||||
for (Role role : roles) {
|
||||
if (role.isAllowedToInit(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAllowedToUpdate(final Set<Role> roles, final Field field) {
|
||||
for (Role role : roles) {
|
||||
if (role.isAllowedToUpdate(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isInitAccess() {
|
||||
return getId() == null;
|
||||
}
|
||||
|
||||
private <F> boolean isActuallyUpdated(final Field field, final T dto, T currentDto) {
|
||||
final Object o1 = ReflectionUtil.getValue(dto, field);
|
||||
final Object o2 = ReflectionUtil.getValue(currentDto, field);
|
||||
if (o1 != null && o2 != null && o1 instanceof Comparable && o2 instanceof Comparable) {
|
||||
return 0 != ((Comparable) o1).compareTo(o2);
|
||||
}
|
||||
return ObjectUtils.notEqual(o1, o2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,14 +2,20 @@
|
||||
package org.hostsharing.hsadminng.service.accessfilter;
|
||||
|
||||
import org.hostsharing.hsadminng.service.UserRoleAssignmentService;
|
||||
import org.hostsharing.hsadminng.service.util.ReflectionUtil;
|
||||
|
||||
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.springframework.context.ApplicationContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A base class for a Spring bean for JSON serialization with field-based access filters.
|
||||
@ -37,7 +43,96 @@ public abstract class JsonSerializerWithAccessFilter<T extends AccessMappings> e
|
||||
final JsonGenerator jsonGenerator,
|
||||
final SerializerProvider serializerProvider) throws IOException {
|
||||
|
||||
new JSonSerializationWithAccessFilter<T>(ctx, userRoleAssignmentService, jsonGenerator, serializerProvider, dto)
|
||||
new JSonSerializationWithAccessFilter(this, ctx, userRoleAssignmentService, jsonGenerator, serializerProvider, dto)
|
||||
.serialize();
|
||||
}
|
||||
|
||||
protected JSonFieldWriter<T> jsonFieldWriter(final Field field) {
|
||||
|
||||
return (final T dto, final JsonGenerator jsonGenerator) -> {
|
||||
final String fieldName = field.getName();
|
||||
final Object fieldValue = ReflectionUtil.getValue(dto, field);
|
||||
// TODO mhoennig turn this into a dispatch table?
|
||||
// TODO mhoennig: or maybe replace by serializerProvider.defaultSerialize...()?
|
||||
// But the latter makes it difficult for parallel structure with the deserializer (clumsy API).
|
||||
// Alternatively extract the supported types to subclasses of some abstract class and
|
||||
// here as well as in the deserializer just access the matching implementation through a map.
|
||||
// Or even completely switch from Jackson to GSON?
|
||||
|
||||
if (fieldValue == null) {
|
||||
jsonGenerator.writeNullField(fieldName);
|
||||
} else if (String.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeStringField(fieldName, (String) fieldValue);
|
||||
} else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeNumberField(fieldName, (int) fieldValue);
|
||||
} else if (Long.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeNumberField(fieldName, (long) fieldValue);
|
||||
} else if (LocalDate.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeStringField(fieldName, fieldValue.toString());
|
||||
} else if (Enum.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeStringField(fieldName, ((Enum) fieldValue).name());
|
||||
} else if (Boolean.class.isAssignableFrom(field.getType()) || boolean.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeBooleanField(fieldName, (Boolean) fieldValue);
|
||||
} else if (BigDecimal.class.isAssignableFrom(field.getType())) {
|
||||
jsonGenerator.writeNumberField(fieldName, (BigDecimal) fieldValue);
|
||||
} else {
|
||||
throw new NotImplementedException("property type not yet implemented: " + field);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL implementation of JSON serialization, where {@link JsonSerializerWithAccessFilter}
|
||||
* is a stateless bean, this inner class exists only during the actual serialization and
|
||||
* contains a serialization state.
|
||||
*/
|
||||
private class JSonSerializationWithAccessFilter extends JSonAccessFilter<T> {
|
||||
|
||||
private final JsonSerializerWithAccessFilter serializer;
|
||||
private final JsonGenerator jsonGenerator;
|
||||
private final SerializerProvider serializerProvider;
|
||||
|
||||
public JSonSerializationWithAccessFilter(
|
||||
final JsonSerializerWithAccessFilter serializer,
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService,
|
||||
final JsonGenerator jsonGenerator,
|
||||
final SerializerProvider serializerProvider,
|
||||
final T dto) {
|
||||
super(ctx, userRoleAssignmentService, dto);
|
||||
this.serializer = serializer;
|
||||
this.jsonGenerator = jsonGenerator;
|
||||
this.serializerProvider = serializerProvider;
|
||||
}
|
||||
|
||||
// Jackson serializes into the JsonGenerator, thus no return value needed.
|
||||
public void serialize() throws IOException {
|
||||
|
||||
jsonGenerator.writeStartObject();
|
||||
for (Field field : dto.getClass().getDeclaredFields()) {
|
||||
toJSon(dto, jsonGenerator, field);
|
||||
}
|
||||
jsonGenerator.writeEndObject();
|
||||
}
|
||||
|
||||
protected void writeJSonField(final T dto, final Field field, final JsonGenerator jsonGenerator) throws IOException {
|
||||
serializer.jsonFieldWriter(field).write(dto, jsonGenerator);
|
||||
}
|
||||
|
||||
private void toJSon(final T dto, final JsonGenerator jsonGenerator, final Field field) throws IOException {
|
||||
if (isAllowedToRead(getLoginUserRoles(), field)) {
|
||||
writeJSonField(dto, field, jsonGenerator);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAllowedToRead(final Set<Role> roles, final Field field) {
|
||||
for (Role role : roles) {
|
||||
if (role.isAllowedToRead(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Role.ANYBODY.isAllowedToRead(field);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -48,15 +48,15 @@ public class MembershipDTO implements AccessMappings, FluentBuilder<MembershipDT
|
||||
@AccessFor(init = Role.ADMIN, read = { Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT })
|
||||
private Long customerId;
|
||||
|
||||
@AccessFor(init = Role.ADMIN, read = { Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT })
|
||||
@AccessFor(update = Role.IGNORED, read = { Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT })
|
||||
private String customerPrefix;
|
||||
|
||||
@AccessFor(init = Role.ANYBODY, update = Role.ANYBODY, read = Role.FINANCIAL_CONTACT)
|
||||
private String displayLabel;
|
||||
|
||||
@AccessFor(init = Role.ANYBODY, update = Role.ANYBODY, read = Role.FINANCIAL_CONTACT)
|
||||
@AccessFor(update = Role.IGNORED, read = Role.FINANCIAL_CONTACT)
|
||||
private String customerDisplayLabel;
|
||||
|
||||
@AccessFor(update = Role.IGNORED, read = Role.FINANCIAL_CONTACT)
|
||||
private String displayLabel;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@ -121,14 +121,6 @@ public class MembershipDTO implements AccessMappings, FluentBuilder<MembershipDT
|
||||
this.customerPrefix = customerPrefix;
|
||||
}
|
||||
|
||||
public String getDisplayLabel() {
|
||||
return displayLabel;
|
||||
}
|
||||
|
||||
public void setDisplayLabel(final String displayLabel) {
|
||||
this.displayLabel = displayLabel;
|
||||
}
|
||||
|
||||
public String getCustomerDisplayLabel() {
|
||||
return customerDisplayLabel;
|
||||
}
|
||||
@ -137,6 +129,14 @@ public class MembershipDTO implements AccessMappings, FluentBuilder<MembershipDT
|
||||
this.customerDisplayLabel = customerDisplayLabel;
|
||||
}
|
||||
|
||||
public String getDisplayLabel() {
|
||||
return displayLabel;
|
||||
}
|
||||
|
||||
public void setDisplayLabel(final String displayLabel) {
|
||||
this.displayLabel = displayLabel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@ -168,14 +168,16 @@ public class MembershipDTO implements AccessMappings, FluentBuilder<MembershipDT
|
||||
", memberUntilDate='" + getMemberUntilDate() + "'" +
|
||||
", remark='" + getRemark() + "'" +
|
||||
", customer=" + getCustomerId() +
|
||||
", customer='" + getCustomerPrefix() + "'" +
|
||||
", customerPrefix='" + getCustomerPrefix() + "'" +
|
||||
", customerDisplayLabel='" + getCustomerDisplayLabel() + "'" +
|
||||
", displayLabel='" + getDisplayLabel() + "'" +
|
||||
"}";
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public static class MembershipJsonSerializer extends JsonSerializerWithAccessFilter<MembershipDTO> {
|
||||
public static class JsonSerializer extends JsonSerializerWithAccessFilter<MembershipDTO> {
|
||||
|
||||
public MembershipJsonSerializer(
|
||||
public JsonSerializer(
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService) {
|
||||
super(ctx, userRoleAssignmentService);
|
||||
@ -183,9 +185,9 @@ public class MembershipDTO implements AccessMappings, FluentBuilder<MembershipDT
|
||||
}
|
||||
|
||||
@JsonComponent
|
||||
public static class MembershipJsonDeserializer extends JsonDeserializerWithAccessFilter<MembershipDTO> {
|
||||
public static class JsonDeserializer extends JsonDeserializerWithAccessFilter<MembershipDTO> {
|
||||
|
||||
public MembershipJsonDeserializer(
|
||||
public JsonDeserializer(
|
||||
final ApplicationContext ctx,
|
||||
final UserRoleAssignmentService userRoleAssignmentService) {
|
||||
super(ctx, userRoleAssignmentService);
|
||||
|
@ -49,7 +49,7 @@ public class JSonAccessFilterTestFixture {
|
||||
}
|
||||
|
||||
@EntityTypeId("test.Given")
|
||||
static class GivenDto implements FluentBuilder<GivenDto> {
|
||||
static class GivenDto implements AccessMappings, FluentBuilder<GivenDto> {
|
||||
|
||||
@SelfId(resolver = GivenService.class)
|
||||
@AccessFor(read = ANYBODY)
|
||||
@ -119,7 +119,7 @@ public class JSonAccessFilterTestFixture {
|
||||
static abstract class GivenChildService implements IdToDtoResolver<GivenChildDto> {
|
||||
}
|
||||
|
||||
public static class GivenChildDto implements FluentBuilder<GivenChildDto> {
|
||||
public static class GivenChildDto implements AccessMappings, FluentBuilder<GivenChildDto> {
|
||||
|
||||
@SelfId(resolver = GivenChildService.class)
|
||||
@AccessFor(read = Role.ANY_CUSTOMER_USER)
|
||||
@ -133,7 +133,7 @@ public class JSonAccessFilterTestFixture {
|
||||
String restrictedField;
|
||||
}
|
||||
|
||||
public static class GivenDtoWithMultipleSelfId {
|
||||
public static class GivenDtoWithMultipleSelfId implements AccessMappings {
|
||||
|
||||
@SelfId(resolver = GivenChildService.class)
|
||||
@AccessFor(read = Role.ANY_CUSTOMER_USER)
|
||||
@ -145,7 +145,7 @@ public class JSonAccessFilterTestFixture {
|
||||
|
||||
}
|
||||
|
||||
public static class GivenDtoWithUnknownFieldType {
|
||||
public static class GivenDtoWithUnknownFieldType implements AccessMappings {
|
||||
|
||||
@SelfId(resolver = GivenChildService.class)
|
||||
@AccessFor(read = Role.ANYBODY)
|
||||
|
@ -20,6 +20,9 @@ public class JSonBuilder {
|
||||
json.append(prop.right);
|
||||
} else if (prop.right instanceof List) {
|
||||
json.append(toJSonArray(prop.right));
|
||||
} else if (prop.right instanceof String && ((String) prop.right).startsWith("{\n")) {
|
||||
// TODO mhoennig: find better solution for adding object nodes
|
||||
json.append(prop.right);
|
||||
} else {
|
||||
json.append(inQuotes(prop.right));
|
||||
}
|
||||
@ -44,12 +47,23 @@ public class JSonBuilder {
|
||||
}
|
||||
|
||||
public JSonBuilder withFieldValueIfPresent(String name, String value) {
|
||||
json.append(value != null ? inQuotes(name) + ":" + inQuotes(value) + "," : "");
|
||||
if (value != null) {
|
||||
json.append(inQuotes(name) + ":" + inQuotes(value) + ",");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSonBuilder withFieldValueIfPresent(String name, Number value) {
|
||||
json.append(value != null ? inQuotes(name) + ":" + value + "," : "");
|
||||
if (value != null) {
|
||||
json.append(inQuotes(name) + ":" + value + ",");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public <E extends Enum<E>> JSonBuilder withFieldValueIfPresent(final String name, final E value) {
|
||||
if (value != null) {
|
||||
json.append(inQuotes(name) + ":" + inQuotes(value.name()) + ",");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -74,5 +88,4 @@ public class JSonBuilder {
|
||||
private static String inQuotes(Object value) {
|
||||
return value != null ? "\"" + value.toString() + "\"" : "null";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.ObjectCodec;
|
||||
import com.fasterxml.jackson.core.TreeNode;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
@ -118,12 +119,7 @@ public class JSonDeserializationWithAccessFilterUnitTest {
|
||||
ImmutablePair.of("openStringField", null)));
|
||||
|
||||
// when
|
||||
GivenDto actualDto = new JSonDeserializationWithAccessFilter<>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
null,
|
||||
GivenDto.class).deserialize();
|
||||
final GivenDto actualDto = deserializerForGivenDto().deserialize(jsonParser, null);
|
||||
|
||||
// then
|
||||
assertThat(actualDto.openStringField).isNull();
|
||||
@ -139,12 +135,7 @@ public class JSonDeserializationWithAccessFilterUnitTest {
|
||||
ImmutablePair.of("openStringField", "String Value")));
|
||||
|
||||
// when
|
||||
GivenDto actualDto = new JSonDeserializationWithAccessFilter<>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
null,
|
||||
GivenDto.class).deserialize();
|
||||
final GivenDto actualDto = deserializerForGivenDto().deserialize(jsonParser, null);
|
||||
|
||||
// then
|
||||
assertThat(actualDto.openStringField).isEqualTo("String Value");
|
||||
@ -160,12 +151,9 @@ public class JSonDeserializationWithAccessFilterUnitTest {
|
||||
ImmutablePair.of("openIntegerField", 1234)));
|
||||
|
||||
// when
|
||||
GivenDto actualDto = new JSonDeserializationWithAccessFilter<>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
null,
|
||||
GivenDto.class).deserialize();
|
||||
// @formatter:off
|
||||
final GivenDto actualDto = deserializerForGivenDto().deserialize(jsonParser, null);;
|
||||
// @formatter:on
|
||||
|
||||
// then
|
||||
assertThat(actualDto.openIntegerField).isEqualTo(1234);
|
||||
@ -182,12 +170,8 @@ public class JSonDeserializationWithAccessFilterUnitTest {
|
||||
ImmutablePair.of("restrictedBigDecimalField", SOME_BIG_DECIMAL_WITH_ANOTHER_SCALE)));
|
||||
|
||||
// when
|
||||
GivenDto actualDto = new JSonDeserializationWithAccessFilter<>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
null,
|
||||
GivenDto.class).deserialize();
|
||||
final GivenDto actualDto = deserializerForGivenDto().deserialize(jsonParser, null);
|
||||
;
|
||||
|
||||
// then
|
||||
assertThat(actualDto.restrictedBigDecimalField).isEqualByComparingTo(SOME_BIG_DECIMAL);
|
||||
@ -217,12 +201,8 @@ public class JSonDeserializationWithAccessFilterUnitTest {
|
||||
ImmutablePair.of("openEnumField", TestEnum.GREEN)));
|
||||
|
||||
// when
|
||||
GivenDto actualDto = new JSonDeserializationWithAccessFilter<>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
null,
|
||||
GivenDto.class).deserialize();
|
||||
final GivenDto actualDto = deserializerForGivenDto().deserialize(jsonParser, null);
|
||||
;
|
||||
|
||||
// then
|
||||
assertThat(actualDto.openIntegerField).isEqualTo(11);
|
||||
@ -247,13 +227,7 @@ public class JSonDeserializationWithAccessFilterUnitTest {
|
||||
ImmutablePair.of("openArrayField", Arrays.asList(11, 22, 33))));
|
||||
|
||||
// when
|
||||
Throwable exception = catchThrowable(
|
||||
() -> new JSonDeserializationWithAccessFilter<>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
null,
|
||||
GivenDto.class).deserialize());
|
||||
Throwable exception = catchThrowable(() -> deserializerForGivenDto().deserialize(jsonParser, null));
|
||||
|
||||
// then
|
||||
assertThat(exception).isInstanceOf(NotImplementedException.class);
|
||||
@ -271,12 +245,7 @@ public class JSonDeserializationWithAccessFilterUnitTest {
|
||||
ImmutablePair.of("restrictedField", "update value of restricted field")));
|
||||
|
||||
// when
|
||||
GivenDto actualDto = new JSonDeserializationWithAccessFilter<>(
|
||||
ctx,
|
||||
userRoleAssignmentService,
|
||||
jsonParser,
|
||||
null,
|
||||