diff --git a/src/main/java/org/hostsharing/hsadminng/domain/User.java b/src/main/java/org/hostsharing/hsadminng/domain/User.java index 1f7ffebf..d16b9f5b 100644 --- a/src/main/java/org/hostsharing/hsadminng/domain/User.java +++ b/src/main/java/org/hostsharing/hsadminng/domain/User.java @@ -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, 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 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; } diff --git a/src/main/java/org/hostsharing/hsadminng/domain/UserRoleAssignment.java b/src/main/java/org/hostsharing/hsadminng/domain/UserRoleAssignment.java index dfe5ea18..cc8453e8 100644 --- a/src/main/java/org/hostsharing/hsadminng/domain/UserRoleAssignment.java +++ b/src/main/java/org/hostsharing/hsadminng/domain/UserRoleAssignment.java @@ -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 { + + public UserRoleAssignmentJsonSerializer( + final ApplicationContext ctx, + final UserRoleAssignmentService userRoleAssignmentService) { + super(ctx, userRoleAssignmentService); + } + + @Override + protected JSonFieldWriter 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 { + + private final UserRepository userRepository; + + public UserRoleAssignmentJsonDeserializer( + final UserRepository userRepository, + final ApplicationContext ctx, + final UserRoleAssignmentService userRoleAssignmentService) { + super(ctx, userRoleAssignmentService); + this.userRepository = userRepository; + } + + @Override + protected JSonFieldReader 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); + } + } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/UserRoleAssignmentService.java b/src/main/java/org/hostsharing/hsadminng/service/UserRoleAssignmentService.java index e9db678d..6f2d7664 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/UserRoleAssignmentService.java +++ b/src/main/java/org/hostsharing/hsadminng/service/UserRoleAssignmentService.java @@ -26,7 +26,7 @@ import java.util.stream.Collectors; */ @Service @Transactional -public class UserRoleAssignmentService { +public class UserRoleAssignmentService implements IdToDtoResolver { private final Logger log = LoggerFactory.getLogger(UserRoleAssignmentService.class); diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java index 7d76f6b3..de844c19 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java @@ -22,7 +22,7 @@ import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; -abstract class JSonAccessFilter { +abstract class JSonAccessFilter { private final ApplicationContext ctx; private final UserRoleAssignmentService userRoleAssignmentService; diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilter.java deleted file mode 100644 index b7f25f39..00000000 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilter.java +++ /dev/null @@ -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 DTO class to serialize - */ -public class JSonDeserializationWithAccessFilter extends JSonAccessFilter { - - private final TreeNode treeNode; - private final Set updatingFields = new HashSet<>(); - - public JSonDeserializationWithAccessFilter( - final ApplicationContext ctx, - final UserRoleAssignmentService userRoleAssignmentService, - final JsonParser jsonParser, - final DeserializationContext deserializationContext, - Class 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 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 roles, final Field field) { - for (Role role : roles) { - if (role.isAllowedToInit(field)) { - return true; - } - } - return false; - } - - private boolean isAllowedToUpdate(final Set 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)); - } -} diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonFieldReader.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonFieldReader.java new file mode 100644 index 00000000..51d1d063 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonFieldReader.java @@ -0,0 +1,19 @@ +// Licensed under Apache-2.0 +package org.hostsharing.hsadminng.service.accessfilter; + +/** + * Reads a JSON node value. + * + * @param + */ +@FunctionalInterface +public interface JSonFieldReader { + + /** + * Reads a JSON node value. + * + * @param target your target entity or DTO type + * + */ + void readInto(T target); +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonFieldWriter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonFieldWriter.java new file mode 100644 index 00000000..6f93a04b --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonFieldWriter.java @@ -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, but declaring IOException as needed by JsonGenerator. + * + * @param + */ +@FunctionalInterface +public interface JSonFieldWriter { + + /** + * 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; +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializationWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializationWithAccessFilter.java deleted file mode 100644 index cf8b7a07..00000000 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializationWithAccessFilter.java +++ /dev/null @@ -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 DTO class to serialize - */ -public class JSonSerializationWithAccessFilter extends JSonAccessFilter { - - 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 roles, final Field field) { - for (Role role : roles) { - if (role.isAllowedToRead(field)) { - return true; - } - } - return Role.ANYBODY.isAllowedToRead(field); - } - -} diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonDeserializerWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonDeserializerWithAccessFilter.java index 85dc4130..3cc464cd 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonDeserializerWithAccessFilter.java @@ -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 extends JsonDeserializer { private final ApplicationContext ctx; @@ -29,11 +44,223 @@ public abstract class JsonDeserializerWithAccessFilter final Class dtoClass = ReflectionUtil .determineGenericClassParameter(this.getClass(), JsonDeserializerWithAccessFilter.class, 0); - return new JSonDeserializationWithAccessFilter( - ctx, - userRoleAssignmentService, - jsonParser, - deserializationContext, - dtoClass).deserialize(); + // @formatter:off + return new JSonDeserializationWithAccessFilter( + this, ctx, userRoleAssignmentService, jsonParser, deserializationContext, dtoClass) + .deserialize(); + // @formatter:on } + + protected JSonFieldReader 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 { + + private final TreeNode treeNode; + private final Set updatingFields = new HashSet<>(); + + public JSonDeserializationWithAccessFilter( + final JsonDeserializerWithAccessFilter deserializer, + final ApplicationContext ctx, + final UserRoleAssignmentService userRoleAssignmentService, + final JsonParser jsonParser, + final DeserializationContext deserializationContext, + Class 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 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 roles, final Field field) { + for (Role role : roles) { + if (role.isAllowedToInit(field)) { + return true; + } + } + return false; + } + + private boolean isAllowedToUpdate(final Set 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) { + 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); + } + } + } diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonSerializerWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonSerializerWithAccessFilter.java index 0991355d..1779d6c9 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JsonSerializerWithAccessFilter.java @@ -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 e final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException { - new JSonSerializationWithAccessFilter(ctx, userRoleAssignmentService, jsonGenerator, serializerProvider, dto) + new JSonSerializationWithAccessFilter(this, ctx, userRoleAssignmentService, jsonGenerator, serializerProvider, dto) .serialize(); } + + protected JSonFieldWriter 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 { + + 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 roles, final Field field) { + for (Role role : roles) { + if (role.isAllowedToRead(field)) { + return true; + } + } + return Role.ANYBODY.isAllowedToRead(field); + } + } + } diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java index 55f9d2f8..f85508ab 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java @@ -48,15 +48,15 @@ public class MembershipDTO implements AccessMappings, FluentBuilder { + public static class JsonSerializer extends JsonSerializerWithAccessFilter { - public MembershipJsonSerializer( + public JsonSerializer( final ApplicationContext ctx, final UserRoleAssignmentService userRoleAssignmentService) { super(ctx, userRoleAssignmentService); @@ -183,9 +185,9 @@ public class MembershipDTO implements AccessMappings, FluentBuilder { + public static class JsonDeserializer extends JsonDeserializerWithAccessFilter { - public MembershipJsonDeserializer( + public JsonDeserializer( final ApplicationContext ctx, final UserRoleAssignmentService userRoleAssignmentService) { super(ctx, userRoleAssignmentService); diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java index 87f03c35..24095f01 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilterTestFixture.java @@ -49,7 +49,7 @@ public class JSonAccessFilterTestFixture { } @EntityTypeId("test.Given") - static class GivenDto implements FluentBuilder { + static class GivenDto implements AccessMappings, FluentBuilder { @SelfId(resolver = GivenService.class) @AccessFor(read = ANYBODY) @@ -119,7 +119,7 @@ public class JSonAccessFilterTestFixture { static abstract class GivenChildService implements IdToDtoResolver { } - public static class GivenChildDto implements FluentBuilder { + public static class GivenChildDto implements AccessMappings, FluentBuilder { @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) diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java index 4935eaac..fc4b95fc 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java @@ -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 > 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"; } - } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilterUnitTest.java index df90f81d..d4eb3722 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializationWithAccessFilterUnitTest.java @@ -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, - GivenDto.class).deserialize(); + final GivenDto actualDto = deserializerForGivenDto().deserialize(jsonParser, null); // then assertThat(actualDto.restrictedField).isEqualTo("update value of restricted field"); @@ -294,12 +263,7 @@ public class JSonDeserializationWithAccessFilterUnitTest { ImmutablePair.of("restrictedField", "initial value of restricted field"))); // when - GivenDto actualDto = new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenDto.class).deserialize(); + final GivenDto actualDto = deserializerForGivenDto().deserialize(jsonParser, null); // then assertThat(actualDto.restrictedField).isEqualTo("initial value of restricted field"); @@ -316,13 +280,7 @@ public class JSonDeserializationWithAccessFilterUnitTest { ImmutablePair.of("restrictedField", "updated value of restricted field"))); // when - Throwable exception = catchThrowable( - () -> new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenDto.class).deserialize()); + final Throwable exception = catchThrowable(() -> deserializerForGivenDto().deserialize(jsonParser, null)); // then assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { @@ -342,13 +300,7 @@ public class JSonDeserializationWithAccessFilterUnitTest { ImmutablePair.of("restrictedField", "another value of restricted field"))); // when - Throwable exception = catchThrowable( - () -> new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenDto.class).deserialize()); + final Throwable exception = catchThrowable(() -> deserializerForGivenDto().deserialize(jsonParser, null)); // then assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { @@ -368,12 +320,7 @@ public class JSonDeserializationWithAccessFilterUnitTest { // when Throwable exception = catchThrowable( - () -> new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenChildDto.class).deserialize()); + () -> deserializerForGivenChildDto().deserialize(jsonParser, null)); // then assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { @@ -392,13 +339,8 @@ public class JSonDeserializationWithAccessFilterUnitTest { ImmutablePair.of("parentId", 1234L))); // when - final GivenChildDto actualDto = new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenChildDto.class) - .deserialize(); + final GivenChildDto actualDto = deserializerForGivenChildDto().deserialize(jsonParser, null); + ; // then assertThat(actualDto.parentId).isEqualTo(1234L); @@ -416,13 +358,8 @@ public class JSonDeserializationWithAccessFilterUnitTest { ImmutablePair.of("restrictedField", "Restricted String Value"))); // when - Throwable exception = catchThrowable( - () -> new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenDto.class).deserialize()); + final Throwable exception = catchThrowable( + () -> deserializerForGivenDto().deserialize(jsonParser, null)); // then assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { @@ -437,14 +374,8 @@ public class JSonDeserializationWithAccessFilterUnitTest { givenJSonTree(asJSon(ImmutablePair.of("id", 1111L))); // when - Throwable exception = catchThrowable( - () -> new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenDtoWithMultipleSelfId.class) - .deserialize()); + final Throwable exception = catchThrowable( + () -> deserializerForGivenDtoWithMultipleSelfId().deserialize(jsonParser, null)); // then assertThat(exception).isInstanceOf(AssertionError.class) @@ -458,14 +389,8 @@ public class JSonDeserializationWithAccessFilterUnitTest { givenJSonTree(asJSon(ImmutablePair.of("unknown", new Arbitrary()))); // when - Throwable exception = catchThrowable( - () -> new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - GivenDtoWithUnknownFieldType.class) - .deserialize()); + final Throwable exception = catchThrowable( + () -> deserializerForGivenDtoWithUnknownFieldType().deserialize(jsonParser, null)); // then assertThat(exception).isInstanceOf(NotImplementedException.class) @@ -482,4 +407,30 @@ public class JSonDeserializationWithAccessFilterUnitTest { given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); } + // We need specialied factories for the deserializer subclasses so that the generic type can be accessed via reflection. + // And it's down here to keep the ugly formatting out of the test cases. + + public JsonDeserializerWithAccessFilter deserializerForGivenDto() throws IOException { + return new JsonDeserializerWithAccessFilter(ctx, userRoleAssignmentService) { + // no need to overload any method here + }; + } + + public JsonDeserializerWithAccessFilter deserializerForGivenChildDto() throws IOException { + return new JsonDeserializerWithAccessFilter(ctx, userRoleAssignmentService) { + // no need to overload any method here + }; + } + + private JsonDeserializer deserializerForGivenDtoWithMultipleSelfId() { + return new JsonDeserializerWithAccessFilter(ctx, userRoleAssignmentService) { + // no need to overload any method here + }; + } + + private JsonDeserializer deserializerForGivenDtoWithUnknownFieldType() { + return new JsonDeserializerWithAccessFilter(ctx, userRoleAssignmentService) { + // no need to overload any method here + }; + } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializationWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializationWithAccessFilterUnitTest.java index fe63f9aa..42d9c97f 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializationWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializationWithAccessFilterUnitTest.java @@ -66,7 +66,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializeStringField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeStringField("openStringField", givenDTO.openStringField); @@ -75,7 +75,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializeIntegerField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeNumberField("openIntegerField", givenDTO.openIntegerField); @@ -84,7 +84,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializePrimitiveIntField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeNumberField("openPrimitiveIntField", givenDTO.openPrimitiveIntField); @@ -93,7 +93,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializeLongField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + final Throwable actual = catchThrowable(() -> serialize(givenDTO)); // then verify(jsonGenerator).writeNumberField("openLongField", givenDTO.openLongField); @@ -102,7 +102,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializePrimitiveLongField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeNumberField("openPrimitiveLongField", givenDTO.openPrimitiveLongField); @@ -111,7 +111,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializeBooleanField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeBooleanField("openBooleanField", givenDTO.openBooleanField); @@ -120,7 +120,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializePrimitiveBooleanField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeBooleanField("openPrimitiveBooleanField", givenDTO.openPrimitiveBooleanField); @@ -129,7 +129,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializeBigDecimalField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + final Throwable actual = catchThrowable(() -> serialize(givenDTO)); // then verify(jsonGenerator).writeNumberField("openBigDecimalField", givenDTO.openBigDecimalField); @@ -138,7 +138,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializeLocalDateField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeStringField("openLocalDateField", givenDTO.openLocalDateFieldAsString); @@ -147,7 +147,7 @@ public class JSonSerializationWithAccessFilterUnitTest { @Test public void shouldSerializeEnumField() throws IOException { // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeStringField("openEnumField", givenDTO.openEnumFieldAsString); @@ -160,7 +160,7 @@ public class JSonSerializationWithAccessFilterUnitTest { securityContext.havingAuthenticatedUser().withRole(GivenCustomerDto.class, 888L, Role.FINANCIAL_CONTACT); // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); @@ -173,7 +173,7 @@ public class JSonSerializationWithAccessFilterUnitTest { securityContext.havingAuthenticatedUser().withRole(GivenCustomerDto.class, 888L, Role.ANY_CUSTOMER_USER); // when - new JSonSerializationWithAccessFilter<>(ctx, userRoleAssignmentService, jsonGenerator, null, givenDTO).serialize(); + serialize(givenDTO); // then verify(jsonGenerator, never()).writeStringField("restrictedField", givenDTO.restrictedField); @@ -184,8 +184,9 @@ public class JSonSerializationWithAccessFilterUnitTest { // given class Arbitrary { + } - class GivenDtoWithUnimplementedFieldType { + class GivenDtoWithUnimplementedFieldType implements AccessMappings { @AccessFor(read = Role.ANYBODY) Arbitrary fieldWithUnimplementedType = new Arbitrary(); @@ -194,14 +195,7 @@ public class JSonSerializationWithAccessFilterUnitTest { SecurityContextFake.havingAuthenticatedUser(); // when - final Throwable actual = catchThrowable( - () -> new JSonSerializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonGenerator, - null, - givenDtoWithUnimplementedFieldType) - .serialize()); + final Throwable actual = catchThrowable(() -> serialize(givenDtoWithUnimplementedFieldType)); // then assertThat(actual).isInstanceOf(NotImplementedException.class); @@ -209,4 +203,10 @@ public class JSonSerializationWithAccessFilterUnitTest { // --- fixture code below --- + public void serialize(final T dto) throws IOException { + // @formatter:off + new JsonSerializerWithAccessFilter(ctx, userRoleAssignmentService) {} + .serialize(dto, jsonGenerator, null); + // @formatter:on + } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOIntTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOIntTest.java new file mode 100644 index 00000000..dffb8cc0 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOIntTest.java @@ -0,0 +1,194 @@ +// Licensed under Apache-2.0 +package org.hostsharing.hsadminng.service.dto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.hostsharing.hsadminng.service.dto.MembershipDTOUnitTest.createSampleDTO; +import static org.junit.Assert.assertEquals; +import static org.mockito.BDDMockito.given; + +import org.hostsharing.hsadminng.domain.Customer; +import org.hostsharing.hsadminng.domain.Membership; +import org.hostsharing.hsadminng.repository.CustomerRepository; +import org.hostsharing.hsadminng.repository.MembershipRepository; +import org.hostsharing.hsadminng.security.AuthoritiesConstants; +import org.hostsharing.hsadminng.service.MembershipService; +import org.hostsharing.hsadminng.service.MembershipValidator; +import org.hostsharing.hsadminng.service.UserRoleAssignmentService; +import org.hostsharing.hsadminng.service.accessfilter.JSonBuilder; +import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.accessfilter.SecurityContextMock; +import org.hostsharing.hsadminng.service.mapper.CustomerMapperImpl; +import org.hostsharing.hsadminng.service.mapper.MembershipMapper; +import org.hostsharing.hsadminng.service.mapper.MembershipMapperImpl; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.commons.lang3.RandomUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; + +import javax.persistence.EntityManager; + +@JsonTest +@SpringBootTest( + classes = { + CustomerMapperImpl.class, + MembershipMapperImpl.class, + MembershipMapperImpl.class, + MembershipDTO.JsonSerializer.class, + MembershipDTO.JsonDeserializer.class + }) +@RunWith(SpringRunner.class) +public class MembershipDTOIntTest { + + private static final Long SOME_CUSTOMER_ID = RandomUtils.nextLong(100, 199); + private static final Integer SOME_CUSTOMER_REFERENCE = 10001; + private static final String SOME_CUSTOMER_PREFIX = "abc"; + private static final String SOME_CUSTOMER_NAME = "Some Customer Name"; + private static final String SOME_CUSTOMER_DISPLAY_LABEL = "Some Customer Name [10001:abc]"; + private static final Customer SOME_CUSTOMER = new Customer().id(SOME_CUSTOMER_ID) + .reference(SOME_CUSTOMER_REFERENCE) + .prefix(SOME_CUSTOMER_PREFIX) + .name(SOME_CUSTOMER_NAME); + + private static final Long SOME_SEPA_MANDATE_ID = RandomUtils.nextLong(300, 399); + private static final Membership SOME_SEPA_MANDATE = new Membership().id(SOME_SEPA_MANDATE_ID).customer(SOME_CUSTOMER); + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MembershipMapper membershipMapper; + + @MockBean + private CustomerRepository customerRepository; + + @MockBean + private MembershipRepository membershipRepository; + + @MockBean + private MembershipValidator membershipValidator; + + @MockBean + private MembershipService MembershipService; + + @MockBean + private EntityManager em; + + @MockBean + public UserRoleAssignmentService userRoleAssignmentService; + + private SecurityContextMock securityContext; + + @Before + public void init() { + given(customerRepository.findById(SOME_CUSTOMER_ID)).willReturn(Optional.of(SOME_CUSTOMER)); + given(membershipRepository.findById(SOME_SEPA_MANDATE_ID)).willReturn((Optional.of(SOME_SEPA_MANDATE))); + + securityContext = SecurityContextMock.usingMock(userRoleAssignmentService); + } + + @Test + public void shouldSerializePartiallyForFinancialCustomerContact() throws JsonProcessingException { + + // given + securityContext.havingAuthenticatedUser().withRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.FINANCIAL_CONTACT); + final MembershipDTO given = createSampleDTO(SOME_SEPA_MANDATE_ID, SOME_CUSTOMER_ID); + + // when + final String actual = objectMapper.writeValueAsString(given); + + // then + given.setRemark(null); + assertEquals(createExpectedJSon(given), actual); + } + + @Test + public void shouldSerializeCompletelyForSupporter() throws JsonProcessingException { + + // given + securityContext.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.SUPPORTER); + final MembershipDTO given = createSampleDTO(SOME_SEPA_MANDATE_ID, SOME_CUSTOMER_ID); + + // when + final String actual = objectMapper.writeValueAsString(given); + + // then + assertEquals(createExpectedJSon(given), actual); + } + + @Test + public void shouldNotDeserializeForContractualCustomerContact() { + // given + securityContext.havingAuthenticatedUser().withRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.CONTRACTUAL_CONTACT); + final String json = new JSonBuilder() + .withFieldValue("id", SOME_SEPA_MANDATE_ID) + .withFieldValue("remark", "Updated Remark") + .toString(); + + // when + final Throwable actual = catchThrowable(() -> objectMapper.readValue(json, MembershipDTO.class)); + + // then + assertThat(actual).isInstanceOfSatisfying( + BadRequestAlertException.class, + bre -> assertThat(bre.getMessage()).isEqualTo( + "Update of field MembershipDTO.remark prohibited for current user role(s): CONTRACTUAL_CONTACT")); + } + + @Test + public void shouldDeserializeForAdminIfRemarkIsChanged() throws IOException { + // given + securityContext.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.ADMIN); + final String json = new JSonBuilder() + .withFieldValue("id", SOME_SEPA_MANDATE_ID) + .withFieldValue("remark", "Updated Remark") + .toString(); + + // when + final MembershipDTO actual = objectMapper.readValue(json, MembershipDTO.class); + + // then + final MembershipDTO expected = new MembershipDTO(); + expected.setId(SOME_SEPA_MANDATE_ID); + expected.setCustomerId(SOME_CUSTOMER_ID); + expected.setRemark("Updated Remark"); + assertThat(actual).isEqualToIgnoringGivenFields(expected, "customerPrefix", "customerDisplayLabel", "displayLabel"); + } + + // --- only test fixture below --- + + private String createExpectedJSon(MembershipDTO dto) { + return new JSonBuilder() + .withFieldValueIfPresent("id", dto.getId()) + .withFieldValueIfPresent("admissionDocumentDate", Objects.toString(dto.getAdmissionDocumentDate())) + .withFieldValueIfPresent("cancellationDocumentDate", Objects.toString(dto.getCancellationDocumentDate())) + .withFieldValueIfPresent("memberFromDate", Objects.toString(dto.getMemberFromDate())) + .withFieldValueIfPresent("memberUntilDate", Objects.toString(dto.getMemberUntilDate())) + .withFieldValueIfPresent("remark", dto.getRemark()) + .withFieldValueIfPresent("customerId", dto.getCustomerId()) + .withFieldValue("customerPrefix", dto.getCustomerPrefix()) + .withFieldValue("customerDisplayLabel", dto.getCustomerDisplayLabel()) + .withFieldValue("displayLabel", dto.getDisplayLabel()) + .toString(); + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOTest.java deleted file mode 100644 index 2c03c8af..00000000 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOTest.java +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed under Apache-2.0 -package org.hostsharing.hsadminng.service.dto; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.Test; - -public class MembershipDTOTest { - - @Test - public void withShouldApplyCallback() { - final MembershipDTO actual = new MembershipDTO().with(m -> m.setRemark("Some Remark")); - - assertThat(actual.getRemark()).isEqualTo("Some Remark"); - } -} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java index 59065852..ce018f50 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java @@ -1,127 +1,104 @@ // Licensed under Apache-2.0 package org.hostsharing.hsadminng.service.dto; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.hostsharing.hsadminng.service.accessfilter.JSonBuilder.asJSon; -import static org.mockito.BDDMockito.given; - -import org.hostsharing.hsadminng.security.AuthoritiesConstants; -import org.hostsharing.hsadminng.service.CustomerService; -import org.hostsharing.hsadminng.service.MembershipService; -import org.hostsharing.hsadminng.service.UserRoleAssignmentService; -import org.hostsharing.hsadminng.service.accessfilter.JSonDeserializationWithAccessFilter; import org.hostsharing.hsadminng.service.accessfilter.Role; -import org.hostsharing.hsadminng.service.accessfilter.SecurityContextMock; -import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.core.TreeNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.junit.Before; -import org.junit.Rule; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomUtils; import org.junit.Test; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; -import org.springframework.context.ApplicationContext; -import java.io.IOException; -import java.util.Optional; +import java.time.LocalDate; -public class MembershipDTOUnitTest { +public class MembershipDTOUnitTest extends AccessMappingsUnitTestBase { - @Rule - public MockitoRule mockitoRule = MockitoJUnit.rule(); - - @Mock - private ApplicationContext ctx; - - @Mock - private AutowireCapableBeanFactory autowireCapableBeanFactory; - - @Mock - private JsonParser jsonParser; - - @Mock - private ObjectCodec codec; - - @Mock - private TreeNode treeNode; - - @Mock - private UserRoleAssignmentService userRoleAssignmentService; - - @Mock - private MembershipService membershipService; - - @Mock - private CustomerService customerService; - - private SecurityContextMock securityContext; - - @Before - public void init() { - given(jsonParser.getCodec()).willReturn(codec); - - given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); - given(autowireCapableBeanFactory.createBean(MembershipService.class)).willReturn(membershipService); - given(autowireCapableBeanFactory.createBean(CustomerService.class)).willReturn(customerService); - given(customerService.findOne(1234L)).willReturn( - Optional.of( - new CustomerDTO() - .with(dto -> dto.setId(1234L)))); - - securityContext = SecurityContextMock.usingMock(userRoleAssignmentService); + public MembershipDTOUnitTest() { + super(MembershipDTO.class, MembershipDTOUnitTest::createSampleDTO, MembershipDTOUnitTest::createRandomDTO); } @Test - public void adminShouldHaveRightToCreate() throws IOException { - securityContext.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.ADMIN); - givenJSonTree(asJSon(ImmutablePair.of("customerId", 1234L))); - - // when - final MembershipDTO actualDto = new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - MembershipDTO.class) - .deserialize(); - - // then - assertThat(actualDto.getCustomerId()).isEqualTo(1234L); + public void shouldHaveProperAccessForAdmin() { + initAccessFor(MembershipDTO.class, Role.ADMIN).shouldBeExactlyFor( + "admissionDocumentDate", + "cancellationDocumentDate", + "memberFromDate", + "memberUntilDate", + "customerId", + "remark"); + updateAccessFor(MembershipDTO.class, Role.ADMIN).shouldBeExactlyFor( + "cancellationDocumentDate", + "memberUntilDate", + "remark"); + readAccessFor(MembershipDTO.class, Role.ADMIN).shouldBeForAllFields(); } @Test - public void contractualContactShouldNotHaveRightToCreate() throws IOException { - securityContext.havingAuthenticatedUser().withRole(CustomerDTO.class, 1234L, Role.CONTRACTUAL_CONTACT); - givenJSonTree(asJSon(ImmutablePair.of("customerId", 1234L))); - - // when - Throwable exception = catchThrowable( - () -> new JSonDeserializationWithAccessFilter<>( - ctx, - userRoleAssignmentService, - jsonParser, - null, - MembershipDTO.class).deserialize()); - - // then - assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { - assertThat(badRequestAlertException.getParam()).isEqualTo("MembershipDTO.customerId"); - assertThat(badRequestAlertException.getErrorKey()).isEqualTo("referencingProhibited"); - }); + public void shouldHaveProperAccessForSupporter() { + initAccessFor(MembershipDTO.class, Role.SUPPORTER).shouldBeForNothing(); + updateAccessFor(MembershipDTO.class, Role.SUPPORTER).shouldBeForNothing(); + readAccessFor(MembershipDTO.class, Role.SUPPORTER).shouldBeForAllFields(); } - // --- only fixture code below --- - - private void givenJSonTree(String givenJSon) throws IOException { - given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); + @Test + public void shouldHaveProperAccessForContractualContact() { + initAccessFor(MembershipDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeForNothing(); + updateAccessFor(MembershipDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeForNothing(); + readAccessFor(MembershipDTO.class, Role.CONTRACTUAL_CONTACT).shouldBeExactlyFor( + "id", + "admissionDocumentDate", + "cancellationDocumentDate", + "memberFromDate", + "memberUntilDate", + "customerId", + "customerPrefix", + "customerDisplayLabel", + "displayLabel"); } + @Test + public void shouldHaveNoAccessForTechnicalContact() { + initAccessFor(MembershipDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing(); + updateAccessFor(MembershipDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing(); + readAccessFor(MembershipDTO.class, Role.TECHNICAL_CONTACT).shouldBeForNothing(); + } + + @Test + public void shouldHaveNoAccessForNormalUsersWithinCustomerRealm() { + initAccessFor(MembershipDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing(); + updateAccessFor(MembershipDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing(); + readAccessFor(MembershipDTO.class, Role.ANY_CUSTOMER_USER).shouldBeForNothing(); + } + + // --- only test fixture below --- + + public static MembershipDTO createSampleDTO(final Long id, final Long parentId) { + final MembershipDTO dto = new MembershipDTO(); + dto.setId(id); + final LocalDate referenceDate = LocalDate.parse("2000-12-07"); + dto.setAdmissionDocumentDate(referenceDate); + dto.setCancellationDocumentDate(referenceDate.plusDays(3500)); + dto.setMemberFromDate(referenceDate.plusDays(4)); + dto.setMemberUntilDate(referenceDate.plusDays(3500).plusDays(400).withDayOfYear(1).minusDays(1)); + dto.setRemark("Some Remark"); + dto.setCustomerId(parentId); + dto.setCustomerPrefix("abc"); + dto.setCustomerDisplayLabel("ABC GmbH [abc:10001]"); + dto.setDisplayLabel("ABC GmbH [abc:10001] 2000-12-11 - 2011-12-31"); + return dto; + } + + public static MembershipDTO createRandomDTO(final Long id, final Long parentId) { + final MembershipDTO dto = new MembershipDTO(); + dto.setId(id); + final LocalDate randomDate = LocalDate.parse("2000-12-07").plusDays(RandomUtils.nextInt(1, 999)); + dto.setAdmissionDocumentDate(randomDate); + dto.setCancellationDocumentDate(randomDate.plusDays(3500)); + dto.setMemberFromDate(randomDate.plusDays(4)); + dto.setMemberUntilDate(randomDate.plusDays(3500).plusDays(400).withDayOfYear(1).minusDays(1)); + dto.setRemark(RandomStringUtils.randomAlphanumeric(20).toUpperCase()); + dto.setCustomerId(parentId); + dto.setCustomerPrefix(RandomStringUtils.randomAlphabetic(3).toLowerCase()); + dto.setCustomerDisplayLabel(RandomStringUtils.randomAlphabetic(13)); + dto.setDisplayLabel(dto.getCustomerDisplayLabel() + dto.getMemberFromDate() + " - " + dto.getMemberUntilDate()); + return dto; + } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/UserRoleAssignmentUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/UserRoleAssignmentUnitTest.java new file mode 100644 index 00000000..d1ffc9d1 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/UserRoleAssignmentUnitTest.java @@ -0,0 +1,154 @@ +// Licensed under Apache-2.0 +package org.hostsharing.hsadminng.service.dto; + +import static org.apache.commons.lang3.tuple.ImmutablePair.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.BDDMockito.given; + +import org.hostsharing.hsadminng.domain.Customer; +import org.hostsharing.hsadminng.domain.User; +import org.hostsharing.hsadminng.domain.UserRoleAssignment; +import org.hostsharing.hsadminng.repository.UserRepository; +import org.hostsharing.hsadminng.repository.UserRoleAssignmentRepository; +import org.hostsharing.hsadminng.security.AuthoritiesConstants; +import org.hostsharing.hsadminng.service.UserRoleAssignmentService; +import org.hostsharing.hsadminng.service.accessfilter.JSonBuilder; +import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.accessfilter.SecurityContextMock; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.util.Optional; + +@JsonTest +@SpringBootTest( + classes = { + UserRoleAssignmentRepository.class, + UserRoleAssignmentService.class, + UserRoleAssignment.UserRoleAssignmentJsonSerializer.class, + UserRoleAssignment.UserRoleAssignmentJsonDeserializer.class }) +@RunWith(SpringRunner.class) +public class UserRoleAssignmentUnitTest { + + public static final long USER_ROLE_ASSIGNMENT_ID = 1234L; + public static final long CUSTOMER_ID = 888L; + public static final long USER_ID = 42L; + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private UserRepository userRepository; + + @MockBean + private UserRoleAssignmentRepository userRoleAssignmentRepository; + + @MockBean + private UserRoleAssignmentService userRoleAssignmentService; + + private SecurityContextMock securityContext; + + @Before + public void init() { + securityContext = SecurityContextMock.usingMock(userRoleAssignmentService); + } + + @Test + public void testSerializationAsContractualCustomerContact() throws JsonProcessingException { + + // given + securityContext.havingAuthenticatedUser().withRole(CustomerDTO.class, CUSTOMER_ID, Role.CONTRACTUAL_CONTACT); + UserRoleAssignment given = createSomeUserRoleAssignment(USER_ROLE_ASSIGNMENT_ID); + + // when + String actual = objectMapper.writeValueAsString(given); + + // then + assertEquals("{}", actual); // dependent rights not yet implemented for UserRoleAssignments + } + + @Test + public void testSerializationAsSupporter() throws JsonProcessingException { + + // given + securityContext.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.SUPPORTER); + UserRoleAssignment given = createSomeUserRoleAssignment(USER_ROLE_ASSIGNMENT_ID); + + // when + String actual = objectMapper.writeValueAsString(given); + + // then + assertThat(actual).isEqualTo(createExpectedJSon(given)); + } + + @Test + public void testDeserializeAsAdmin() throws IOException { + // given + securityContext.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.ADMIN); + given(userRoleAssignmentRepository.findById(USER_ROLE_ASSIGNMENT_ID)) + .willReturn(Optional.of(new UserRoleAssignment().id(USER_ROLE_ASSIGNMENT_ID))); + final User expectedUser = new User().id(USER_ID); + given(userRepository.getOne(USER_ID)).willReturn(expectedUser); + String json = JSonBuilder.asJSon( + of("id", USER_ROLE_ASSIGNMENT_ID), + of("entityTypeId", Customer.ENTITY_TYPE_ID), + of("entityObjectId", CUSTOMER_ID), + of( + "user", + JSonBuilder.asJSon( + of("id", USER_ID))), + of("assignedRole", Role.TECHNICAL_CONTACT.name())); + + // when + UserRoleAssignment actual = objectMapper.readValue(json, UserRoleAssignment.class); + + // then + UserRoleAssignment expected = new UserRoleAssignment(); + expected.setId(USER_ROLE_ASSIGNMENT_ID); + expected.setEntityTypeId(Customer.ENTITY_TYPE_ID); + expected.setEntityObjectId(CUSTOMER_ID); + expected.setAssignedRole(Role.TECHNICAL_CONTACT); + expected.setUser(expectedUser); + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + // --- only test fixture below --- + + public static String createExpectedJSon(UserRoleAssignment dto) { + return new JSonBuilder() + .withFieldValueIfPresent("id", dto.getId()) + .withFieldValueIfPresent("entityTypeId", dto.getEntityTypeId()) + .withFieldValueIfPresent("entityObjectId", dto.getEntityObjectId()) + .withFieldValueIfPresent("assignedRole", dto.getAssignedRole()) + .withFieldValueIfPresent("user", dto.getUser().getId()) + .toString(); + } + + public static UserRoleAssignment createSomeUserRoleAssignment(final Long id) { + final UserRoleAssignment given = new UserRoleAssignment(); + given.setId(id); + given.setEntityTypeId(Customer.ENTITY_TYPE_ID); + given.setEntityObjectId(CUSTOMER_ID); + given.setUser(new User().id(USER_ID)); + given.setAssignedRole(Role.TECHNICAL_CONTACT); + return given; + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/web/rest/MembershipResourceIntTest.java b/src/test/java/org/hostsharing/hsadminng/web/rest/MembershipResourceIntTest.java index dd03fe7b..7d757ce0 100644 --- a/src/test/java/org/hostsharing/hsadminng/web/rest/MembershipResourceIntTest.java +++ b/src/test/java/org/hostsharing/hsadminng/web/rest/MembershipResourceIntTest.java @@ -180,6 +180,9 @@ public class MembershipResourceIntTest { // Create the Membership MembershipDTO membershipDTO = membershipMapper.toDto(membership); + membershipDTO.setCustomerPrefix(null); + membershipDTO.setCustomerDisplayLabel(null); + membershipDTO.setDisplayLabel(null); restMembershipMockMvc.perform( post("/api/memberships") .contentType(TestUtil.APPLICATION_JSON_UTF8) diff --git a/src/test/java/org/hostsharing/hsadminng/web/rest/MembershipResourceUnitTest.java b/src/test/java/org/hostsharing/hsadminng/web/rest/MembershipResourceUnitTest.java new file mode 100644 index 00000000..89db87df --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/web/rest/MembershipResourceUnitTest.java @@ -0,0 +1,58 @@ +// Licensed under Apache-2.0 +package org.hostsharing.hsadminng.web.rest; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; + +import org.hostsharing.hsadminng.service.dto.MembershipDTO; +import org.hostsharing.hsadminng.service.dto.MembershipDTOUnitTest; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +// Currently this class tests mostly special 'bad paths' +// which make little sense to test in *ResourceIntTest. +public class MembershipResourceUnitTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @InjectMocks + private MembershipResource membershipResource; + + @Test + public void createSepaMandateWithoutIdThrowsBadRequestException() { + + // given + final MembershipDTO givenDto = MembershipDTOUnitTest.createRandomDTO(null, 1L); + + // when + final Throwable actual = catchThrowable(() -> membershipResource.updateMembership(givenDto)); + + // then + assertThat(actual).isInstanceOfSatisfying(BadRequestAlertException.class, bre -> { + assertThat(bre.getErrorKey()).isEqualTo("idnull"); + assertThat(bre.getParam()).isEqualTo("membership"); + }); + } + + @Test + public void createSepaMandateWithIdThrowsBadRequestException() { + + // given + final MembershipDTO givenDto = MembershipDTOUnitTest.createRandomDTO(2L, 1L); + + // when + final Throwable actual = catchThrowable(() -> membershipResource.createMembership(givenDto)); + + // then + assertThat(actual).isInstanceOfSatisfying(BadRequestAlertException.class, bre -> { + assertThat(bre.getErrorKey()).isEqualTo("idexists"); + assertThat(bre.getParam()).isEqualTo("membership"); + }); + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/web/rest/UserRoleAssignmentResourceIntTest.java b/src/test/java/org/hostsharing/hsadminng/web/rest/UserRoleAssignmentResourceIntTest.java index 55114319..d9e55518 100644 --- a/src/test/java/org/hostsharing/hsadminng/web/rest/UserRoleAssignmentResourceIntTest.java +++ b/src/test/java/org/hostsharing/hsadminng/web/rest/UserRoleAssignmentResourceIntTest.java @@ -11,9 +11,11 @@ import org.hostsharing.hsadminng.HsadminNgApp; import org.hostsharing.hsadminng.domain.User; import org.hostsharing.hsadminng.domain.UserRoleAssignment; import org.hostsharing.hsadminng.repository.UserRoleAssignmentRepository; +import org.hostsharing.hsadminng.security.AuthoritiesConstants; import org.hostsharing.hsadminng.service.UserRoleAssignmentQueryService; import org.hostsharing.hsadminng.service.UserRoleAssignmentService; import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.accessfilter.SecurityContextFake; import org.hostsharing.hsadminng.web.rest.errors.ExceptionTranslator; import org.junit.Before; @@ -94,6 +96,8 @@ public class UserRoleAssignmentResourceIntTest { .setMessageConverters(jacksonMessageConverter) .setValidator(validator) .build(); + + SecurityContextFake.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.SUPPORTER); } /** @@ -103,9 +107,13 @@ public class UserRoleAssignmentResourceIntTest { * if they test an entity which requires the current entity. */ public static UserRoleAssignment createEntity(EntityManager em) { + User user = UserResourceIntTest.createEntity(em); + em.persist(user); + em.flush(); UserRoleAssignment userRoleAssignment = new UserRoleAssignment() .entityTypeId(DEFAULT_ENTITY_TYPE_ID) .entityObjectId(DEFAULT_ENTITY_OBJECT_ID) + .user(user) .assignedRole(DEFAULT_ASSIGNED_ROLE); return userRoleAssignment; } @@ -121,6 +129,7 @@ public class UserRoleAssignmentResourceIntTest { int databaseSizeBeforeCreate = userRoleAssignmentRepository.findAll().size(); // Create the UserRoleAssignment + SecurityContextFake.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.ADMIN); restUserRoleAssignmentMockMvc.perform( post("/api/user-role-assignments") .contentType(TestUtil.APPLICATION_JSON_UTF8) @@ -460,6 +469,7 @@ public class UserRoleAssignmentResourceIntTest { int databaseSizeBeforeUpdate = userRoleAssignmentRepository.findAll().size(); // Update the userRoleAssignment + SecurityContextFake.havingAuthenticatedUser().withAuthority(AuthoritiesConstants.ADMIN); UserRoleAssignment updatedUserRoleAssignment = userRoleAssignmentRepository.findById(userRoleAssignment.getId()).get(); // Disconnect from session so that the updates on updatedUserRoleAssignment are not directly saved in db em.detach(updatedUserRoleAssignment); diff --git a/src/test/java/org/hostsharing/hsadminng/web/rest/UserRoleAssignmentResourceUnitTest.java b/src/test/java/org/hostsharing/hsadminng/web/rest/UserRoleAssignmentResourceUnitTest.java new file mode 100644 index 00000000..d5774a42 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/web/rest/UserRoleAssignmentResourceUnitTest.java @@ -0,0 +1,58 @@ +// Licensed under Apache-2.0 +package org.hostsharing.hsadminng.web.rest; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; + +import org.hostsharing.hsadminng.domain.UserRoleAssignment; +import org.hostsharing.hsadminng.service.dto.UserRoleAssignmentUnitTest; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +// Currently this class tests mostly special 'bad paths' +// which make little sense to test in *ResourceIntTest. +public class UserRoleAssignmentResourceUnitTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @InjectMocks + private UserRoleAssignmentResource userRoleAssignmentResource; + + @Test + public void createUserRoleAssignmentWithoutIdThrowsBadRequestException() { + + // given + final UserRoleAssignment givenEntity = UserRoleAssignmentUnitTest.createSomeUserRoleAssignment(null); + + // when + final Throwable actual = catchThrowable(() -> userRoleAssignmentResource.updateUserRoleAssignment(givenEntity)); + + // then + assertThat(actual).isInstanceOfSatisfying(BadRequestAlertException.class, bre -> { + assertThat(bre.getErrorKey()).isEqualTo("idnull"); + assertThat(bre.getParam()).isEqualTo("userRoleAssignment"); + }); + } + + @Test + public void createUserRoleAssignmentWithIdThrowsBadRequestException() { + + // given + final UserRoleAssignment givenEntity = UserRoleAssignmentUnitTest.createSomeUserRoleAssignment(1L); + + // when + final Throwable actual = catchThrowable(() -> userRoleAssignmentResource.createUserRoleAssignment(givenEntity)); + + // then + assertThat(actual).isInstanceOfSatisfying(BadRequestAlertException.class, bre -> { + assertThat(bre.getErrorKey()).isEqualTo("idexists"); + assertThat(bre.getParam()).isEqualTo("userRoleAssignment"); + }); + } +}