From 35565e1b43c47dc1ab5dbed5fd25b1a4896fa1cf Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 17 Apr 2019 16:45:16 +0200 Subject: [PATCH 01/24] comment access rights on CustomerDTO --- build.gradle | 2 +- .../hostsharing/hsadminng/service/dto/CustomerDTO.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 542e5db4..7efa4725 100644 --- a/build.gradle +++ b/build.gradle @@ -208,7 +208,7 @@ task cucumberTestReport(type: TestReport) { pitest { targetClasses = ['org.hostsharing.hsadminng.*'] - threads = 4 + threads = 2 // Do not set these limit even lower, they are already pretty bad values! // 83%*78% means that only ~66% of the code is properly covered by automated tests. diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index ff8bf3d8..1e98bfec 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -25,18 +25,25 @@ public class CustomerDTO implements Serializable { @NotNull @Size(max = 400) + // visible by >=contractual contact + // changeable by >=supporter private String contractualAddress; @Size(max = 80) + // visible by >=contractual contact + // changeable by >=supporter private String contractualSalutation; @Size(max = 400) + // visible by >=contractual contact | >=billing contact + // changeable by >=contractual contact private String billingAddress; @Size(max = 80) + // visible by >=contractual contact | >=billing contact + // changeable by >=contractual contact private String billingSalutation; - public Long getId() { return id; } From bc87739d6fa0c3607ded93a765fab332bc1149d6 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 18 Apr 2019 14:48:58 +0200 Subject: [PATCH 02/24] JSON serialiezr/deserializer for CustomerDTO - manually --- .../hsadminng/service/dto/CustomerDTO.java | 171 +++++++++++++++++- .../service/dto/CustomerDTOUnitTest.java | 113 ++++++++++++ 2 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index 1e98bfec..ce713409 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -1,13 +1,41 @@ package org.hostsharing.hsadminng.service.dto; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.apache.commons.lang3.NotImplementedException; +import org.hostsharing.hsadminng.security.SecurityUtils; +import org.springframework.boot.jackson.JsonComponent; + +import javax.annotation.PostConstruct; import javax.validation.constraints.*; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.IOException; import java.io.Serializable; +import java.lang.annotation.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Objects; +import java.util.Optional; + /** * A DTO for the Customer entity. */ +@ReadableFor(Role.ANY_CUSTOMER_USER) +@WritableFor(Role.SUPPORTER) public class CustomerDTO implements Serializable { + @WritableFor(Role.NOBODY) private Long id; @NotNull @@ -25,23 +53,21 @@ public class CustomerDTO implements Serializable { @NotNull @Size(max = 400) - // visible by >=contractual contact - // changeable by >=supporter + @ReadableFor(Role.CONTRACTUAL_CONTACT) private String contractualAddress; @Size(max = 80) - // visible by >=contractual contact - // changeable by >=supporter + @ReadableFor(Role.CONTRACTUAL_CONTACT) + @WritableFor(Role.CONTRACTUAL_CONTACT) private String contractualSalutation; @Size(max = 400) - // visible by >=contractual contact | >=billing contact - // changeable by >=contractual contact + @ReadableFor(Role.CONTRACTUAL_CONTACT) private String billingAddress; @Size(max = 80) - // visible by >=contractual contact | >=billing contact - // changeable by >=contractual contact + @ReadableFor(Role.CONTRACTUAL_CONTACT) + @WritableFor(Role.CONTRACTUAL_CONTACT) private String billingSalutation; public Long getId() { @@ -142,4 +168,133 @@ public class CustomerDTO implements Serializable { ", billingSalutation='" + getBillingSalutation() + "'" + "}"; } + + @JsonComponent + public static class CustomerJsonSerializer extends JsonSerializer { + + private Optional login; + + @PostConstruct + public void getLoginUser() { + this.login = SecurityUtils.getCurrentUserLogin(); + } + + @Override + public void serialize(CustomerDTO dto, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + + jsonGenerator.writeStartObject(); + try { + for (PropertyDescriptor prop : Introspector.getBeanInfo(CustomerDTO.class).getPropertyDescriptors()) { + if (isRealProprety(prop)) { + toJSon(dto, jsonGenerator, prop); + } + } + } catch (IntrospectionException e) { + throw new RuntimeException(e); + } + +// +// jsonGenerator.writeNumberField("number", dto.getNumber()); +// jsonGenerator.writeStringField("prefix", dto.getPrefix()); +// jsonGenerator.writeStringField("name", dto.getName()); +// toJSonString(dto, jsonGenerator,"contractualAddress"); +// jsonGenerator.writeStringField("contractualSalutation", dto.getContractualSalutation()); +// jsonGenerator.writeStringField("billingAddress", dto.getBillingAddress()); +// jsonGenerator.writeStringField("billingSalutation", dto.getBillingSalutation()); + + jsonGenerator.writeEndObject(); + } + + private boolean isRealProprety(PropertyDescriptor prop) { + return prop.getWriteMethod() != null; + } + + private void toJSonString(CustomerDTO user, JsonGenerator jsonGenerator, String fieldName) throws IOException { + if (isReadAllowed(fieldName)) { + jsonGenerator.writeStringField(fieldName, user.getContractualAddress()); + } + } + + private void toJSon(CustomerDTO dto, JsonGenerator jsonGenerator, PropertyDescriptor prop) throws IOException { + final String fieldName = prop.getName(); + if (isReadAllowed(fieldName)) { + if (Integer.class.isAssignableFrom(prop.getPropertyType()) || int.class.isAssignableFrom(prop.getPropertyType())) { + jsonGenerator.writeNumberField(fieldName, (int) invoke(dto, prop.getReadMethod())); + } else if (Long.class.isAssignableFrom(prop.getPropertyType()) || long.class.isAssignableFrom(prop.getPropertyType())) { + jsonGenerator.writeNumberField(fieldName, (long) invoke(dto, prop.getReadMethod())); + } else if (String.class.isAssignableFrom(prop.getPropertyType())) { + jsonGenerator.writeStringField(fieldName, (String) invoke(dto, prop.getReadMethod())); + } else { + throw new NotImplementedException("property type not yet implemented" + prop); + } + } + } + + private Object invoke(Object dto, Method method) { + try { + return method.invoke(dto); + } catch (IllegalAccessException|InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private boolean isReadAllowed(String fieldName) { + if ( fieldName.equals("contractualAddress") ) { + return login.map(user -> user.equals("admin")).orElse(false); + } + return true; + } + } + + @JsonComponent + public static class UserJsonDeserializer extends JsonDeserializer { + + @Override + public CustomerDTO deserialize(JsonParser jsonParser, + DeserializationContext deserializationContext) throws IOException, + JsonProcessingException { + + TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser); + + CustomerDTO dto = new CustomerDTO(); + dto.setId(((IntNode) treeNode.get("id")).asLong()); + dto.setNumber(((IntNode) treeNode.get("number")).asInt()); + dto.setPrefix(((TextNode) treeNode.get("prefix")).asText()); + dto.setName(((TextNode) treeNode.get("name")).asText()); + dto.setContractualAddress(((TextNode) treeNode.get("contractualAddress")).asText()); + dto.setContractualSalutation(((TextNode) treeNode.get("contractualSalutation")).asText()); + dto.setBillingAddress(((TextNode) treeNode.get("billingAddress")).asText()); + dto.setBillingSalutation(((TextNode) treeNode.get("billingSalutation")).asText()); + + return dto; + } + } +} + +enum Role { + NOBODY(0), HOSTMASTER(1), ADMIN(2), SUPPORTER(3), + ANY_CUSTOMER_CONTACT(10), CONTRACTUAL_CONTACT(11), + ANY_CUSTOMER_USER(30); + + private final int level; + + Role(final int level) { + this.level = level; + } +} + +@Target({ElementType.FIELD, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@interface ReadableFor { + +} + + +@Target({ElementType.FIELD, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@interface WritableFor { + } diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java new file mode 100644 index 00000000..472a235e --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -0,0 +1,113 @@ +package org.hostsharing.hsadminng.service.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hostsharing.hsadminng.security.SecurityUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@JsonTest +@RunWith(SpringRunner.class) +public class CustomerDTOUnitTest { + + + @Autowired + private ObjectMapper objectMapper; + + @Test + public void testSerializationAsCustomer() throws JsonProcessingException { + + // given + CustomerDTO given = createSomeCustomerDTO(); + givenLoginUser("customer"); + + // when + String actual = objectMapper.writeValueAsString(given); + + // then + given.setContractualAddress(null); + //given.setContractualSalutation(null); + assertEquals(createExpectedJSon(given), actual); + } + + @Test + public void testDeserializeAsCustomer() throws IOException { + // given + String json = "{\"id\":1234,\"number\":10001,\"prefix\":\"abc\",\"name\":\"Mein Name\",\"contractualAddress\":\"Eine Adresse\",\"contractualSalutation\":\"Hallo\",\"billingAddress\":\"Noch eine Adresse\",\"billingSalutation\":\"Moin\"}"; + givenLoginUser("customer"); + + // when + CustomerDTO actual = objectMapper.readValue(json, CustomerDTO.class); + + // then + CustomerDTO expected = new CustomerDTO(); + expected.setId(1234L); + expected.setNumber(10001); + expected.setPrefix("abc"); + expected.setName("Mein Name"); + expected.setContractualAddress(null); // not allowed + expected.setContractualSalutation("Hallo"); + expected.setBillingAddress("Noch eine Adresse"); + expected.setBillingSalutation("Moin"); + assertEquals(actual, expected); + } + + private String createExpectedJSon(CustomerDTO dto) { + String json = // the fields in alphanumeric order: + toJSonFieldDefinitionIfPresent("billingAddress", dto.getBillingAddress()) + + toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()) + + toJSonFieldDefinitionIfPresent("contractualAddress", dto.getContractualAddress()) + + toJSonFieldDefinitionIfPresent("contractualSalutation", dto.getContractualSalutation()) + + toJSonFieldDefinitionIfPresent("id", dto.getId()) + + toJSonFieldDefinitionIfPresent("name", dto.getName()) + + toJSonFieldDefinitionIfPresent("number", dto.getNumber()) + + toJSonFieldDefinitionIfPresent("prefix", dto.getPrefix()); + return "{" + json.substring(0, json.length() - 1) + "}"; + } + + private String toJSonFieldDefinitionIfPresent(String name, String value) { + return value != null ? inQuotes(name) + ":" + inQuotes(value) + "," : ""; + } + + private String toJSonFieldDefinitionIfPresent(String name, Number value) { + return value != null ? inQuotes(name) + ":" + value + "," : ""; + } + + private String inQuotes(Object value) { + return "\"" + value.toString() + "\""; + } + + private CustomerDTO createSomeCustomerDTO() { + CustomerDTO given = new CustomerDTO(); + given.setId(1234L); + given.setNumber(10001); + given.setPrefix("abc"); + given.setName("Mein Name"); + given.setContractualAddress("Eine Adresse"); + given.setContractualSalutation("Hallo"); + given.setBillingAddress("Noch eine Adresse"); + given.setBillingSalutation("Moin"); + givenLoginUser("admin"); + return given; + } + + private void givenLoginUser(String userName) { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(userName, userName)); + SecurityContextHolder.setContext(securityContext); + Optional login = SecurityUtils.getCurrentUserLogin(); + assertThat(login).describedAs("precondition failed").contains(userName); + } +} From 5287d78fcb384e981c90dc8b4f4195d14c196671 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 18 Apr 2019 17:12:24 +0200 Subject: [PATCH 03/24] JSON serializer generalized --- package-lock.json | 41 +++-- .../hsadminng/service/dto/CustomerDTO.java | 166 ++++++++++-------- .../service/dto/CustomerDTOUnitTest.java | 42 +++-- 3 files changed, 150 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96002d14..b13a9116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6025,7 +6025,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6046,12 +6047,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6066,17 +6069,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6193,7 +6199,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6205,6 +6212,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6219,6 +6227,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6226,12 +6235,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6250,6 +6261,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6330,7 +6342,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6342,6 +6355,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6427,7 +6441,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -6463,6 +6478,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6482,6 +6498,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6525,12 +6542,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index ce713409..e56b5ac3 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -14,60 +14,55 @@ import org.apache.commons.lang3.NotImplementedException; import org.hostsharing.hsadminng.security.SecurityUtils; import org.springframework.boot.jackson.JsonComponent; -import javax.annotation.PostConstruct; import javax.validation.constraints.*; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; import java.io.IOException; import java.io.Serializable; import java.lang.annotation.*; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; -import java.util.Optional; /** * A DTO for the Customer entity. */ -@ReadableFor(Role.ANY_CUSTOMER_USER) -@WritableFor(Role.SUPPORTER) public class CustomerDTO implements Serializable { - @WritableFor(Role.NOBODY) + @AccessFor(read = Role.ANY_CUSTOMER_USER) private Long id; @NotNull @Min(value = 10000) @Max(value = 99999) + @AccessFor(init = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) private Integer number; @NotNull @Pattern(regexp = "[a-z][a-z0-9]+") + @AccessFor(init = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) private String prefix; @NotNull @Size(max = 80) + @AccessFor(init = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) private String name; @NotNull @Size(max = 400) - @ReadableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.CONTRACTUAL_CONTACT) private String contractualAddress; @Size(max = 80) - @ReadableFor(Role.CONTRACTUAL_CONTACT) - @WritableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.CONTRACTUAL_CONTACT, read = Role.ANY_CUSTOMER_CONTACT) private String contractualSalutation; @Size(max = 400) - @ReadableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.CONTRACTUAL_CONTACT) private String billingAddress; @Size(max = 80) - @ReadableFor(Role.CONTRACTUAL_CONTACT) - @WritableFor(Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}, read = Role.CONTRACTUAL_CONTACT) private String billingSalutation; public Long getId() { @@ -172,79 +167,54 @@ public class CustomerDTO implements Serializable { @JsonComponent public static class CustomerJsonSerializer extends JsonSerializer { - private Optional login; - - @PostConstruct - public void getLoginUser() { - this.login = SecurityUtils.getCurrentUserLogin(); - } - @Override public void serialize(CustomerDTO dto, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeStartObject(); - try { - for (PropertyDescriptor prop : Introspector.getBeanInfo(CustomerDTO.class).getPropertyDescriptors()) { - if (isRealProprety(prop)) { - toJSon(dto, jsonGenerator, prop); - } - } - } catch (IntrospectionException e) { - throw new RuntimeException(e); + for (Field prop : CustomerDTO.class.getDeclaredFields()) { + toJSon(dto, jsonGenerator, prop); } -// -// jsonGenerator.writeNumberField("number", dto.getNumber()); -// jsonGenerator.writeStringField("prefix", dto.getPrefix()); -// jsonGenerator.writeStringField("name", dto.getName()); -// toJSonString(dto, jsonGenerator,"contractualAddress"); -// jsonGenerator.writeStringField("contractualSalutation", dto.getContractualSalutation()); -// jsonGenerator.writeStringField("billingAddress", dto.getBillingAddress()); -// jsonGenerator.writeStringField("billingSalutation", dto.getBillingSalutation()); - jsonGenerator.writeEndObject(); } - private boolean isRealProprety(PropertyDescriptor prop) { - return prop.getWriteMethod() != null; - } - - private void toJSonString(CustomerDTO user, JsonGenerator jsonGenerator, String fieldName) throws IOException { - if (isReadAllowed(fieldName)) { - jsonGenerator.writeStringField(fieldName, user.getContractualAddress()); - } - } - - private void toJSon(CustomerDTO dto, JsonGenerator jsonGenerator, PropertyDescriptor prop) throws IOException { - final String fieldName = prop.getName(); - if (isReadAllowed(fieldName)) { - if (Integer.class.isAssignableFrom(prop.getPropertyType()) || int.class.isAssignableFrom(prop.getPropertyType())) { - jsonGenerator.writeNumberField(fieldName, (int) invoke(dto, prop.getReadMethod())); - } else if (Long.class.isAssignableFrom(prop.getPropertyType()) || long.class.isAssignableFrom(prop.getPropertyType())) { - jsonGenerator.writeNumberField(fieldName, (long) invoke(dto, prop.getReadMethod())); - } else if (String.class.isAssignableFrom(prop.getPropertyType())) { - jsonGenerator.writeStringField(fieldName, (String) invoke(dto, prop.getReadMethod())); + private void toJSon(CustomerDTO dto, JsonGenerator jsonGenerator, Field prop) throws IOException { + if (getLoginUserRole().isAllowedToRead(prop)) { + final String fieldName = prop.getName(); + if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); + } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeNumberField(fieldName, (long) get(dto, prop)); + } else if (String.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); } else { throw new NotImplementedException("property type not yet implemented" + prop); } } } - private Object invoke(Object dto, Method method) { + private Object get(CustomerDTO dto, Field field) { try { - return method.invoke(dto); - } catch (IllegalAccessException|InvocationTargetException e) { + field.setAccessible(true); + return field.get(dto); + } catch (IllegalAccessException e) { throw new RuntimeException(e); } } - private boolean isReadAllowed(String fieldName) { - if ( fieldName.equals("contractualAddress") ) { - return login.map(user -> user.equals("admin")).orElse(false); - } - return true; + private Role getLoginUserRole() { + return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); } + + private Object invoke(Object dto, Method method) { + try { + return method.invoke(dto); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } @JsonComponent @@ -274,27 +244,73 @@ public class CustomerDTO implements Serializable { enum Role { NOBODY(0), HOSTMASTER(1), ADMIN(2), SUPPORTER(3), - ANY_CUSTOMER_CONTACT(10), CONTRACTUAL_CONTACT(11), - ANY_CUSTOMER_USER(30); + ANY_CUSTOMER_CONTACT(20), CONTRACTUAL_CONTACT(21), FINANCIAL_CONTACT(22), TECHNICAL_CONTACT(22), + ANY_CUSTOMER_USER(80), + ANYBODY(99); private final int level; Role(final int level) { this.level = level; } + + boolean covers(final Role role) { + return this == role || this.level < role.level; + } + + public boolean isAllowedToInit(Field field) { + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.init()); + } + + public boolean isAllowedToUpdate(Field field) { + + final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.update()); + } + + public boolean isAllowedToRead(Field field) { + + final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.read()); + } + + private boolean isRoleCovered(Role[] requiredRoles) { + for (Role accessAllowedForRole : requiredRoles) { + if (this.covers(accessAllowedForRole)) { + return true; + } + } + return false; + } + } @Target({ElementType.FIELD, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Documented -@interface ReadableFor { +@interface AccessFor { + Role[] init() default Role.NOBODY; + Role[] update() default Role.NOBODY; + + Role[] read() default Role.NOBODY; } - -@Target({ElementType.FIELD, ElementType.TYPE_USE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@interface WritableFor { - -} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index 472a235e..b6be38c9 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -27,26 +27,42 @@ public class CustomerDTOUnitTest { private ObjectMapper objectMapper; @Test - public void testSerializationAsCustomer() throws JsonProcessingException { + public void testSerializationAsContractualCustomerContact() throws JsonProcessingException { // given CustomerDTO given = createSomeCustomerDTO(); - givenLoginUser("customer"); + givenLoginUserWithRole("ANY_CUSTOMER_USER"); // when String actual = objectMapper.writeValueAsString(given); // then given.setContractualAddress(null); - //given.setContractualSalutation(null); + given.setContractualSalutation(null); + given.setBillingAddress(null); + given.setBillingSalutation(null); assertEquals(createExpectedJSon(given), actual); } @Test - public void testDeserializeAsCustomer() throws IOException { + public void testSerializationAsSupporter() throws JsonProcessingException { + + // given + CustomerDTO given = createSomeCustomerDTO(); + givenLoginUserWithRole("SUPPORTER"); + + // when + String actual = objectMapper.writeValueAsString(given); + + // then + assertEquals(createExpectedJSon(given), actual); + } + + @Test + public void testDeserializeAsContractualCustomerContact() throws IOException { // given String json = "{\"id\":1234,\"number\":10001,\"prefix\":\"abc\",\"name\":\"Mein Name\",\"contractualAddress\":\"Eine Adresse\",\"contractualSalutation\":\"Hallo\",\"billingAddress\":\"Noch eine Adresse\",\"billingSalutation\":\"Moin\"}"; - givenLoginUser("customer"); + givenLoginUserWithRole("CONTRACTUAL_CONTACT"); // when CustomerDTO actual = objectMapper.readValue(json, CustomerDTO.class); @@ -66,14 +82,14 @@ public class CustomerDTOUnitTest { private String createExpectedJSon(CustomerDTO dto) { String json = // the fields in alphanumeric order: - toJSonFieldDefinitionIfPresent("billingAddress", dto.getBillingAddress()) + - toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()) + + toJSonFieldDefinitionIfPresent("id", dto.getId()) + + toJSonFieldDefinitionIfPresent("number", dto.getNumber()) + + toJSonFieldDefinitionIfPresent("prefix", dto.getPrefix()) + + toJSonFieldDefinitionIfPresent("name", dto.getName()) + toJSonFieldDefinitionIfPresent("contractualAddress", dto.getContractualAddress()) + toJSonFieldDefinitionIfPresent("contractualSalutation", dto.getContractualSalutation()) + - toJSonFieldDefinitionIfPresent("id", dto.getId()) + - toJSonFieldDefinitionIfPresent("name", dto.getName()) + - toJSonFieldDefinitionIfPresent("number", dto.getNumber()) + - toJSonFieldDefinitionIfPresent("prefix", dto.getPrefix()); + toJSonFieldDefinitionIfPresent("billingAddress", dto.getBillingAddress()) + + toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()); return "{" + json.substring(0, json.length() - 1) + "}"; } @@ -99,11 +115,11 @@ public class CustomerDTOUnitTest { given.setContractualSalutation("Hallo"); given.setBillingAddress("Noch eine Adresse"); given.setBillingSalutation("Moin"); - givenLoginUser("admin"); + givenLoginUserWithRole("admin"); return given; } - private void givenLoginUser(String userName) { + private void givenLoginUserWithRole(String userName) { SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(userName, userName)); SecurityContextHolder.setContext(securityContext); From ce083e928a04f48ef15fd6d7eedda958cca43705 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 17 Apr 2019 16:45:16 +0200 Subject: [PATCH 04/24] comment access rights on CustomerDTO --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 542e5db4..7efa4725 100644 --- a/build.gradle +++ b/build.gradle @@ -208,7 +208,7 @@ task cucumberTestReport(type: TestReport) { pitest { targetClasses = ['org.hostsharing.hsadminng.*'] - threads = 4 + threads = 2 // Do not set these limit even lower, they are already pretty bad values! // 83%*78% means that only ~66% of the code is properly covered by automated tests. From 6ab67995ff3a9834c0c64ef64690af388b36f965 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 18 Apr 2019 18:11:56 +0200 Subject: [PATCH 05/24] fixing tests after merging master --- .../hsadminng/service/dto/CustomerDTO.java | 19 +++++++------------ .../service/dto/CustomerDTOUnitTest.java | 19 +++++++++++-------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index 368e9038..de2a6ba3 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -2,7 +2,6 @@ package org.hostsharing.hsadminng.service.dto; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; @@ -57,10 +56,6 @@ public class CustomerDTO implements Serializable { @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.CONTRACTUAL_CONTACT) private String contractualAddress; - @Size(max = 80) - @AccessFor(init = Role.ADMIN, update = Role.CONTRACTUAL_CONTACT, read = Role.ANY_CUSTOMER_CONTACT) - private String contractualSalutation; - @Size(max = 80) @AccessFor(init = Role.ADMIN, update = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}, read = Role.CONTRACTUAL_CONTACT) private String billingSalutation; @@ -182,10 +177,10 @@ public class CustomerDTO implements Serializable { } @JsonComponent - public static class CustomerJsonSerializer extends JsonSerializer { + public static class JsonSerializerWithAccessFilter extends JsonSerializer { @Override - public void serialize(CustomerDTO dto, JsonGenerator jsonGenerator, + public void serialize(Object dto, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeStartObject(); @@ -196,7 +191,7 @@ public class CustomerDTO implements Serializable { jsonGenerator.writeEndObject(); } - private void toJSon(CustomerDTO dto, JsonGenerator jsonGenerator, Field prop) throws IOException { + private void toJSon(Object dto, JsonGenerator jsonGenerator, Field prop) throws IOException { if (getLoginUserRole().isAllowedToRead(prop)) { final String fieldName = prop.getName(); if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { @@ -211,7 +206,7 @@ public class CustomerDTO implements Serializable { } } - private Object get(CustomerDTO dto, Field field) { + private Object get(Object dto, Field field) { try { field.setAccessible(true); return field.get(dto); @@ -239,20 +234,20 @@ public class CustomerDTO implements Serializable { @Override public CustomerDTO deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) throws IOException, - JsonProcessingException { + DeserializationContext deserializationContext) throws IOException { TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser); CustomerDTO dto = new CustomerDTO(); dto.setId(((IntNode) treeNode.get("id")).asLong()); - dto.setNumber(((IntNode) treeNode.get("number")).asInt()); + dto.setReference(((IntNode) treeNode.get("reference")).asInt()); dto.setPrefix(((TextNode) treeNode.get("prefix")).asText()); dto.setName(((TextNode) treeNode.get("name")).asText()); dto.setContractualAddress(((TextNode) treeNode.get("contractualAddress")).asText()); dto.setContractualSalutation(((TextNode) treeNode.get("contractualSalutation")).asText()); dto.setBillingAddress(((TextNode) treeNode.get("billingAddress")).asText()); dto.setBillingSalutation(((TextNode) treeNode.get("billingSalutation")).asText()); + dto.setRemark(((TextNode) treeNode.get("remark")).asText()); return dto; } diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index b6be38c9..1ad1cc5d 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -41,6 +41,7 @@ public class CustomerDTOUnitTest { given.setContractualSalutation(null); given.setBillingAddress(null); given.setBillingSalutation(null); + given.setRemark(null); assertEquals(createExpectedJSon(given), actual); } @@ -55,13 +56,13 @@ public class CustomerDTOUnitTest { String actual = objectMapper.writeValueAsString(given); // then - assertEquals(createExpectedJSon(given), actual); + assertThat(actual).isEqualTo(createExpectedJSon(given)); } @Test public void testDeserializeAsContractualCustomerContact() throws IOException { // given - String json = "{\"id\":1234,\"number\":10001,\"prefix\":\"abc\",\"name\":\"Mein Name\",\"contractualAddress\":\"Eine Adresse\",\"contractualSalutation\":\"Hallo\",\"billingAddress\":\"Noch eine Adresse\",\"billingSalutation\":\"Moin\"}"; + String json = "{\"id\":1234,\"reference\":10001,\"prefix\":\"abc\",\"name\":\"Mein Name\",\"contractualAddress\":\"Eine Adresse\",\"contractualSalutation\":\"Hallo\",\"billingAddress\":\"Noch eine Adresse\",\"billingSalutation\":\"Moin\",\"remark\":\"Eine Bemerkung\"}"; givenLoginUserWithRole("CONTRACTUAL_CONTACT"); // when @@ -70,26 +71,28 @@ public class CustomerDTOUnitTest { // then CustomerDTO expected = new CustomerDTO(); expected.setId(1234L); - expected.setNumber(10001); + expected.setReference(10001); expected.setPrefix("abc"); expected.setName("Mein Name"); expected.setContractualAddress(null); // not allowed expected.setContractualSalutation("Hallo"); expected.setBillingAddress("Noch eine Adresse"); expected.setBillingSalutation("Moin"); + expected.setRemark("Eine Bemerkung"); assertEquals(actual, expected); } private String createExpectedJSon(CustomerDTO dto) { String json = // the fields in alphanumeric order: toJSonFieldDefinitionIfPresent("id", dto.getId()) + - toJSonFieldDefinitionIfPresent("number", dto.getNumber()) + + toJSonFieldDefinitionIfPresent("reference", dto.getReference()) + toJSonFieldDefinitionIfPresent("prefix", dto.getPrefix()) + toJSonFieldDefinitionIfPresent("name", dto.getName()) + - toJSonFieldDefinitionIfPresent("contractualAddress", dto.getContractualAddress()) + toJSonFieldDefinitionIfPresent("contractualSalutation", dto.getContractualSalutation()) + + toJSonFieldDefinitionIfPresent("contractualAddress", dto.getContractualAddress()) + + toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()) + toJSonFieldDefinitionIfPresent("billingAddress", dto.getBillingAddress()) + - toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()); + toJSonFieldDefinitionIfPresent("remark", dto.getRemark()) ; return "{" + json.substring(0, json.length() - 1) + "}"; } @@ -108,14 +111,14 @@ public class CustomerDTOUnitTest { private CustomerDTO createSomeCustomerDTO() { CustomerDTO given = new CustomerDTO(); given.setId(1234L); - given.setNumber(10001); + given.setReference(10001); given.setPrefix("abc"); given.setName("Mein Name"); given.setContractualAddress("Eine Adresse"); given.setContractualSalutation("Hallo"); given.setBillingAddress("Noch eine Adresse"); given.setBillingSalutation("Moin"); - givenLoginUserWithRole("admin"); + given.setRemark("Eine Bemerkung"); return given; } From f4960f260e2e440d9b91547f28a05cd6ce15ca7f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 19 Apr 2019 07:18:20 +0200 Subject: [PATCH 06/24] moving the JSonSerializer to a separate package --- .../service/accessfilter/AccessFor.java | 16 ++++ .../JSonSerializerWithAccessFilter.java | 68 +++++++++++++++++ .../hsadminng/service/accessfilter/Role.java | 66 +++++++++++++++++ .../hsadminng/service/dto/CustomerDTO.java | 74 +------------------ .../service/dto/CustomerDTOUnitTest.java | 1 - 5 files changed, 152 insertions(+), 73 deletions(-) create mode 100644 src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java create mode 100644 src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java create mode 100644 src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java new file mode 100644 index 00000000..d25b5c87 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java @@ -0,0 +1,16 @@ +package org.hostsharing.hsadminng.service.accessfilter; + + +import java.lang.annotation.*; + +@Target({ElementType.FIELD, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AccessFor { + Role[] init() default Role.NOBODY; + + Role[] update() default Role.NOBODY; + + Role[] read() default Role.NOBODY; +} + diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java new file mode 100644 index 00000000..76a4734b --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -0,0 +1,68 @@ +package org.hostsharing.hsadminng.service.accessfilter; + + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.apache.commons.lang3.NotImplementedException; +import org.hostsharing.hsadminng.security.SecurityUtils; +import org.hostsharing.hsadminng.service.dto.CustomerDTO; +import org.springframework.boot.jackson.JsonComponent; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +@JsonComponent +public class JSonSerializerWithAccessFilter extends JsonSerializer { + + @Override + public void serialize(Object dto, JsonGenerator jsonGenerator, + SerializerProvider serializerProvider) throws IOException { + + jsonGenerator.writeStartObject(); + for (Field prop : CustomerDTO.class.getDeclaredFields()) { + toJSon(dto, jsonGenerator, prop); + } + + jsonGenerator.writeEndObject(); + } + + private void toJSon(Object dto, JsonGenerator jsonGenerator, Field prop) throws IOException { + if (getLoginUserRole().isAllowedToRead(prop)) { + final String fieldName = prop.getName(); + if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); + } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeNumberField(fieldName, (long) get(dto, prop)); + } else if (String.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); + } else { + throw new NotImplementedException("property type not yet implemented" + prop); + } + } + } + + private Object get(Object dto, Field field) { + try { + field.setAccessible(true); + return field.get(dto); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private Role getLoginUserRole() { + return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + } + + private Object invoke(Object dto, Method method) { + try { + return method.invoke(dto); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java new file mode 100644 index 00000000..aaf1f473 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java @@ -0,0 +1,66 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import org.hostsharing.hsadminng.security.SecurityUtils; + +import java.lang.reflect.Field; + +public enum Role { + NOBODY(0), HOSTMASTER(1), ADMIN(2), SUPPORTER(3), + ANY_CUSTOMER_CONTACT(20), CONTRACTUAL_CONTACT(21), FINANCIAL_CONTACT(22), TECHNICAL_CONTACT(22), + ANY_CUSTOMER_USER(80), + ANYBODY(99); + + private final int level; + + Role(final int level) { + this.level = level; + } + + boolean covers(final Role role) { + return this == role || this.level < role.level; + } + + public boolean isAllowedToInit(Field field) { + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.init()); + } + + public boolean isAllowedToUpdate(Field field) { + + final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.update()); + } + + public boolean isAllowedToRead(Field field) { + + final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + + final AccessFor accessFor = field.getAnnotation(AccessFor.class); + if (accessFor == null) { + return false; + } + + return isRoleCovered(accessFor.read()); + } + + private boolean isRoleCovered(Role[] requiredRoles) { + for (Role accessAllowedForRole : requiredRoles) { + if (this.covers(accessAllowedForRole)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index de2a6ba3..e7ba3fe5 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -11,6 +11,8 @@ import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.TextNode; import org.apache.commons.lang3.NotImplementedException; import org.hostsharing.hsadminng.security.SecurityUtils; +import org.hostsharing.hsadminng.service.accessfilter.AccessFor; +import org.hostsharing.hsadminng.service.accessfilter.Role; import org.springframework.boot.jackson.JsonComponent; import javax.validation.constraints.*; @@ -254,75 +256,3 @@ public class CustomerDTO implements Serializable { } } -enum Role { - NOBODY(0), HOSTMASTER(1), ADMIN(2), SUPPORTER(3), - ANY_CUSTOMER_CONTACT(20), CONTRACTUAL_CONTACT(21), FINANCIAL_CONTACT(22), TECHNICAL_CONTACT(22), - ANY_CUSTOMER_USER(80), - ANYBODY(99); - - private final int level; - - Role(final int level) { - this.level = level; - } - - boolean covers(final Role role) { - return this == role || this.level < role.level; - } - - public boolean isAllowedToInit(Field field) { - - final AccessFor accessFor = field.getAnnotation(AccessFor.class); - if (accessFor == null) { - return false; - } - - return isRoleCovered(accessFor.init()); - } - - public boolean isAllowedToUpdate(Field field) { - - final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); - - final AccessFor accessFor = field.getAnnotation(AccessFor.class); - if (accessFor == null) { - return false; - } - - return isRoleCovered(accessFor.update()); - } - - public boolean isAllowedToRead(Field field) { - - final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); - - final AccessFor accessFor = field.getAnnotation(AccessFor.class); - if (accessFor == null) { - return false; - } - - return isRoleCovered(accessFor.read()); - } - - private boolean isRoleCovered(Role[] requiredRoles) { - for (Role accessAllowedForRole : requiredRoles) { - if (this.covers(accessAllowedForRole)) { - return true; - } - } - return false; - } - -} - -@Target({ElementType.FIELD, ElementType.TYPE_USE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@interface AccessFor { - Role[] init() default Role.NOBODY; - - Role[] update() default Role.NOBODY; - - Role[] read() default Role.NOBODY; -} - diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index 1ad1cc5d..6ef88849 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.assertEquals; @RunWith(SpringRunner.class) public class CustomerDTOUnitTest { - @Autowired private ObjectMapper objectMapper; From d5a37ddfae7df256b75a7a7f7103bb7a401e5f7d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 19 Apr 2019 07:46:11 +0200 Subject: [PATCH 07/24] JavaDoc for Role --- .../hsadminng/service/accessfilter/Role.java | 98 +++++++++++++++++-- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java index aaf1f473..b1b2d648 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java @@ -4,10 +4,63 @@ import org.hostsharing.hsadminng.security.SecurityUtils; import java.lang.reflect.Field; +/** + * These enum values are on the one hand used to define the minimum role required to grant access to resources, + * but on the other hand also for the roles users can be assigned to. + */ public enum Role { - NOBODY(0), HOSTMASTER(1), ADMIN(2), SUPPORTER(3), - ANY_CUSTOMER_CONTACT(20), CONTRACTUAL_CONTACT(21), FINANCIAL_CONTACT(22), TECHNICAL_CONTACT(22), + /** + * Default for access rights requirement. You can read it as: 'Nobody is allowed to ...'. + * This is usually used for fields which are managed by hsadminNg itself. + */ + NOBODY(0), + + /** + * Hostmasters are initialize/update/read and field which, except where NOBODY is allowed to. + */ + HOSTMASTER(1), + + /** + * This role is for administrators, e.g. to create memberships and book shared and assets. + */ + ADMIN(2), + + /** + * This role is for members of the support team. + */ + SUPPORTER(3), + + /** + * This meta-role is to specify that any kind of customer contact can get access to the resource. + */ + ANY_CUSTOMER_CONTACT(20), + + /** + * This role is for contractual contacts of a customer, like a director of the company. + * Who has this role, has the broadest access to all resources which belong to this customer. + * Everything which relates to the contract with the customer, needs this role. + */ + CONTRACTUAL_CONTACT(21), + + /** + * This role is for financial contacts of a customer, e.g. for accessing billing data. + */ + FINANCIAL_CONTACT(22), + + /** + * This role is for technical contacts of a customer. + */ + TECHNICAL_CONTACT(22), + + /** + * Any user which belongs to a customer has at least this role. + */ ANY_CUSTOMER_USER(80), + + /** + * This role is meant to specify that a resources can be accessed by anybody, even without login. + * It's currently only used for technical purposes. + */ ANYBODY(99); private final int level; @@ -16,11 +69,30 @@ public enum Role { this.level = level; } + /** + * Determines if the given role is covered by this role. + * + * Where 'this' means the Java instance itself as a role of a system user. + * + * @example + * Role.HOSTMASTER.covers(Role.ANY_CUSTOMER_USER) == true + * + * @param role The required role for a resource. + * + * @return whether this role comprises the given role + */ boolean covers(final Role role) { return this == role || this.level < role.level; } - public boolean isAllowedToInit(Field field) { + /** + * Checks if this role of a user allows to initialize the given field when creating the resource. + * + * @param field a field of the DTO of a resource + * + * @return true if allowed + */ + public boolean isAllowedToInit(final Field field) { final AccessFor accessFor = field.getAnnotation(AccessFor.class); if (accessFor == null) { @@ -30,7 +102,14 @@ public enum Role { return isRoleCovered(accessFor.init()); } - public boolean isAllowedToUpdate(Field field) { + /** + * Checks if this role of a user allows to update the given field. + * + * @param field a field of the DTO of a resource + * + * @return true if allowed + */ + public boolean isAllowedToUpdate(final Field field) { final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); @@ -42,7 +121,14 @@ public enum Role { return isRoleCovered(accessFor.update()); } - public boolean isAllowedToRead(Field field) { + /** + * Checks if this role of a user allows to read the given field. + * + * @param field a field of the DTO of a resource + * + * @return true if allowed + */ + public boolean isAllowedToRead(final Field field) { final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); @@ -54,7 +140,7 @@ public enum Role { return isRoleCovered(accessFor.read()); } - private boolean isRoleCovered(Role[] requiredRoles) { + private boolean isRoleCovered(final Role[] requiredRoles) { for (Role accessAllowedForRole : requiredRoles) { if (this.covers(accessAllowedForRole)) { return true; From 1ad74907bd1247c1fad0414e947a51e967f042d0 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 19 Apr 2019 09:13:19 +0200 Subject: [PATCH 08/24] RoleUnitTest + special case FINANCIAL_CUSTOMER_CONTACT --- .../hsadminng/service/accessfilter/Role.java | 24 +++++- .../service/accessfilter/RoleUnitTest.java | 83 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java index b1b2d648..81ff1078 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java @@ -7,6 +7,11 @@ import java.lang.reflect.Field; /** * These enum values are on the one hand used to define the minimum role required to grant access to resources, * but on the other hand also for the roles users can be assigned to. + * + * TODO: Maybe splitting it up into UserRole and RequiredRole would make it more clear? + * And maybe instead of a level, we could then add the comprised roles in the constructor? + * This could also be a better way to express that the financial contact has no rights to + * other users resources (see also ACTUAL_CUSTOMER_USEr vs. ANY_CUSTOMER_USER). */ public enum Role { /** @@ -45,17 +50,32 @@ public enum Role { /** * This role is for financial contacts of a customer, e.g. for accessing billing data. */ - FINANCIAL_CONTACT(22), + FINANCIAL_CONTACT(22) { + @Override + boolean covers(final Role role) { + if (role == ACTUAL_CUSTOMER_USER) { + return false; + } + return super.covers(role); + } + }, /** * This role is for technical contacts of a customer. */ TECHNICAL_CONTACT(22), + /** * Any user which belongs to a customer has at least this role. */ - ANY_CUSTOMER_USER(80), + ACTUAL_CUSTOMER_USER(80), + + /** + * Use this to grant rights to any user, also special function users who have no + * rights on other users resources. + */ + ANY_CUSTOMER_USER(89), /** * This role is meant to specify that a resources can be accessed by anybody, even without login. diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java new file mode 100644 index 00000000..d1fe7481 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java @@ -0,0 +1,83 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RoleUnitTest { + + @Test + public void allUserRolesShouldCoverSameRequiredRole() { + assertThat(Role.HOSTMASTER.covers(Role.HOSTMASTER)).isTrue(); + assertThat(Role.ADMIN.covers(Role.ADMIN)).isTrue(); + assertThat(Role.SUPPORTER.covers(Role.SUPPORTER)).isTrue(); + + assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isTrue(); + assertThat(Role.FINANCIAL_CONTACT.covers(Role.FINANCIAL_CONTACT)).isTrue(); + assertThat(Role.TECHNICAL_CONTACT.covers(Role.TECHNICAL_CONTACT)).isTrue(); + + + assertThat(Role.ACTUAL_CUSTOMER_USER.covers((Role.ACTUAL_CUSTOMER_USER))).isTrue(); + assertThat(Role.ANY_CUSTOMER_USER.covers((Role.ANY_CUSTOMER_USER))).isTrue(); + } + + @Test + public void lowerUserRolesShouldNotCoverHigherRequiredRoles() { + assertThat(Role.HOSTMASTER.covers(Role.NOBODY)).isFalse(); + assertThat(Role.ADMIN.covers(Role.HOSTMASTER)).isFalse(); + assertThat(Role.SUPPORTER.covers(Role.ADMIN)).isFalse(); + + assertThat(Role.ANY_CUSTOMER_CONTACT.covers(Role.SUPPORTER)).isFalse(); + assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.ANY_CUSTOMER_CONTACT)).isFalse(); + assertThat(Role.FINANCIAL_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isFalse(); + assertThat(Role.FINANCIAL_CONTACT.covers(Role.TECHNICAL_CONTACT)).isFalse(); + assertThat(Role.TECHNICAL_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isFalse(); + assertThat(Role.TECHNICAL_CONTACT.covers(Role.FINANCIAL_CONTACT)).isFalse(); + + assertThat(Role.ACTUAL_CUSTOMER_USER.covers((Role.ANY_CUSTOMER_CONTACT))).isFalse(); + assertThat(Role.ACTUAL_CUSTOMER_USER.covers((Role.CONTRACTUAL_CONTACT))).isFalse(); + assertThat(Role.ACTUAL_CUSTOMER_USER.covers((Role.TECHNICAL_CONTACT))).isFalse(); + assertThat(Role.ACTUAL_CUSTOMER_USER.covers((Role.FINANCIAL_CONTACT))).isFalse(); + + assertThat(Role.ANY_CUSTOMER_USER.covers((Role.ACTUAL_CUSTOMER_USER))).isFalse(); + assertThat(Role.ANY_CUSTOMER_USER.covers((Role.ANY_CUSTOMER_CONTACT))).isFalse(); + assertThat(Role.ANY_CUSTOMER_USER.covers((Role.CONTRACTUAL_CONTACT))).isFalse(); + assertThat(Role.ANY_CUSTOMER_USER.covers((Role.TECHNICAL_CONTACT))).isFalse(); + assertThat(Role.ANY_CUSTOMER_USER.covers((Role.FINANCIAL_CONTACT))).isFalse(); + + assertThat(Role.ANYBODY.covers((Role.ANY_CUSTOMER_USER))).isFalse(); + } + + @Test + public void higherUserRolesShouldCoverLowerRequiredRoles() { + assertThat(Role.HOSTMASTER.covers(Role.SUPPORTER)).isTrue(); + assertThat(Role.ADMIN.covers(Role.SUPPORTER)).isTrue(); + + assertThat(Role.SUPPORTER.covers(Role.ANY_CUSTOMER_CONTACT)).isTrue(); + + assertThat(Role.ANY_CUSTOMER_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isTrue(); + assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.FINANCIAL_CONTACT)).isTrue(); + assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.TECHNICAL_CONTACT)).isTrue(); + assertThat(Role.TECHNICAL_CONTACT.covers(Role.ANY_CUSTOMER_USER)).isTrue(); + + assertThat(Role.ACTUAL_CUSTOMER_USER.covers((Role.ANY_CUSTOMER_USER))).isTrue(); + assertThat(Role.ANY_CUSTOMER_USER.covers((Role.ANYBODY))).isTrue(); + } + + @Test + public void financialContactShouldNotCoverAnyCustomersUsersRoleRequirement() { + assertThat(Role.FINANCIAL_CONTACT.covers(Role.ACTUAL_CUSTOMER_USER)).isFalse(); + } + + @Test + public void isAllowedToInit() { + } + + @Test + public void isAllowedToUpdate() { + } + + @Test + public void isAllowedToRead() { + } +} From a24ca35bd7417acae9cd9b5c16324c09f0993c1f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 19 Apr 2019 10:46:40 +0200 Subject: [PATCH 09/24] JSonSerializerWithAccessFilterUnitTest --- .../JSonSerializerWithAccessFilter.java | 24 ++-- ...SonSerializerWithAccessFilterUnitTest.java | 114 ++++++++++++++++++ .../accessfilter/MockSecurityContext.java | 24 ++++ .../service/dto/CustomerDTOUnitTest.java | 16 +-- 4 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java create mode 100644 src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java 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 76a4734b..2fd08c51 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.apache.commons.lang3.NotImplementedException; import org.hostsharing.hsadminng.security.SecurityUtils; -import org.hostsharing.hsadminng.service.dto.CustomerDTO; import org.springframework.boot.jackson.JsonComponent; import java.io.IOException; @@ -18,20 +17,22 @@ import java.lang.reflect.Method; public class JSonSerializerWithAccessFilter extends JsonSerializer { @Override - public void serialize(Object dto, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { + public void serialize(final Object dto, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) throws IOException { + // TODO: move the implementation to an (if necessary, inner) class jsonGenerator.writeStartObject(); - for (Field prop : CustomerDTO.class.getDeclaredFields()) { + for (Field prop : dto.getClass().getDeclaredFields()) { toJSon(dto, jsonGenerator, prop); } jsonGenerator.writeEndObject(); } - private void toJSon(Object dto, JsonGenerator jsonGenerator, Field prop) throws IOException { + private void toJSon(final Object dto, final JsonGenerator jsonGenerator, final Field prop) throws IOException { if (getLoginUserRole().isAllowedToRead(prop)) { final String fieldName = prop.getName(); + // TODO: maybe replace by serializerProvider.defaultSerialize...()? if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { @@ -44,25 +45,16 @@ public class JSonSerializerWithAccessFilter extends JsonSerializer { } } - private Object get(Object dto, Field field) { + private Object get(final Object dto, final Field field) { try { field.setAccessible(true); return field.get(dto); } catch (IllegalAccessException e) { - throw new RuntimeException(e); + throw new RuntimeException("getting field " + field + " failed", e); } } private Role getLoginUserRole() { return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); } - - private Object invoke(Object dto, Method method) { - try { - return method.invoke(dto); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java new file mode 100644 index 00000000..9a5e838f --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -0,0 +1,114 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import com.fasterxml.jackson.core.JsonGenerator; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class JSonSerializerWithAccessFilterUnitTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + public JsonGenerator jsonGenerator; + + private final GivenDto givenDTO = createSampleDto(); + + @Before + public void init() { + givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + } + + @Test + public void shouldSerializeStringField() throws IOException { + // when + new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + + // then + verify(jsonGenerator).writeStringField("openStringField", givenDTO.openStringField); + } + + @Test + public void shouldSerializeRestrictedFieldIfRequiredRoleIsCoveredByUser() throws IOException { + + // given + givenLoginUserWithRole(Role.FINANCIAL_CONTACT); + + // when + new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + + // then + verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); + } + + @Test + public void shouldNotSerializeRestrictedFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + + // given + givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + + // when + new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + + // then + verify(jsonGenerator, never()).writeStringField("restrictedField", givenDTO.restrictedField); + } + + @Test + public void shouldThrowExceptionForUnimplementedFieldType() throws IOException { + + // given + class Arbitrary {} + class GivenDtoWithUnimplementedFieldType { + @AccessFor(read = Role.ANYBODY) + Arbitrary fieldWithUnimplementedType; + } + final GivenDtoWithUnimplementedFieldType givenDto = new GivenDtoWithUnimplementedFieldType(); + + // when + Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter().serialize(givenDto, jsonGenerator, null)); + + // then + assertThat(actual).isInstanceOf(NotImplementedException.class); + } + + // --- fixture code below --- + + private GivenDto createSampleDto() { + final GivenDto dto = new GivenDto(); + dto.restrictedField = RandomStringUtils.randomAlphabetic(10); + dto.openStringField = RandomStringUtils.randomAlphabetic(10); + dto.openIntegerField = RandomUtils.nextInt(); + dto.openLongField = RandomUtils.nextLong(); + return dto; + } + + private static class GivenDto { + @AccessFor(read = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) + String restrictedField; + + @AccessFor(read = Role.ANYBODY) + String openStringField; + + @AccessFor(read = Role.ANYBODY) + Integer openIntegerField; + + @AccessFor(read = Role.ANYBODY) + Long openLongField; + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java new file mode 100644 index 00000000..5a656c77 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java @@ -0,0 +1,24 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import org.hostsharing.hsadminng.security.SecurityUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MockSecurityContext { + + public static void givenLoginUserWithRole(final Role userRole) { + final String fakeUserName = userRole.name(); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(fakeUserName, "dummy")); + SecurityContextHolder.setContext(securityContext); + Optional login = SecurityUtils.getCurrentUserLogin(); + + assertThat(login).describedAs("precondition failed").contains(fakeUserName); + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index 6ef88849..7686c234 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -3,6 +3,7 @@ package org.hostsharing.hsadminng.service.dto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.hostsharing.hsadminng.security.SecurityUtils; +import org.hostsharing.hsadminng.service.accessfilter.Role; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +17,7 @@ import java.io.IOException; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; import static org.junit.Assert.assertEquals; @JsonTest @@ -30,7 +32,7 @@ public class CustomerDTOUnitTest { // given CustomerDTO given = createSomeCustomerDTO(); - givenLoginUserWithRole("ANY_CUSTOMER_USER"); + givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); // when String actual = objectMapper.writeValueAsString(given); @@ -49,7 +51,7 @@ public class CustomerDTOUnitTest { // given CustomerDTO given = createSomeCustomerDTO(); - givenLoginUserWithRole("SUPPORTER"); + givenLoginUserWithRole(Role.SUPPORTER); // when String actual = objectMapper.writeValueAsString(given); @@ -62,7 +64,7 @@ public class CustomerDTOUnitTest { public void testDeserializeAsContractualCustomerContact() throws IOException { // given String json = "{\"id\":1234,\"reference\":10001,\"prefix\":\"abc\",\"name\":\"Mein Name\",\"contractualAddress\":\"Eine Adresse\",\"contractualSalutation\":\"Hallo\",\"billingAddress\":\"Noch eine Adresse\",\"billingSalutation\":\"Moin\",\"remark\":\"Eine Bemerkung\"}"; - givenLoginUserWithRole("CONTRACTUAL_CONTACT"); + givenLoginUserWithRole(Role.CONTRACTUAL_CONTACT); // when CustomerDTO actual = objectMapper.readValue(json, CustomerDTO.class); @@ -120,12 +122,4 @@ public class CustomerDTOUnitTest { given.setRemark("Eine Bemerkung"); return given; } - - private void givenLoginUserWithRole(String userName) { - SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(userName, userName)); - SecurityContextHolder.setContext(securityContext); - Optional login = SecurityUtils.getCurrentUserLogin(); - assertThat(login).describedAs("precondition failed").contains(userName); - } } From 6e017aba496fd32ff10c89568899130aa126aabb Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 19 Apr 2019 11:01:52 +0200 Subject: [PATCH 10/24] removing orphaned specialized serializer from CustomerDTO --- .../hsadminng/service/dto/CustomerDTO.java | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index e7ba3fe5..18b6e793 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -178,59 +178,6 @@ public class CustomerDTO implements Serializable { "}"; } - @JsonComponent - public static class JsonSerializerWithAccessFilter extends JsonSerializer { - - @Override - public void serialize(Object dto, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { - - jsonGenerator.writeStartObject(); - for (Field prop : CustomerDTO.class.getDeclaredFields()) { - toJSon(dto, jsonGenerator, prop); - } - - jsonGenerator.writeEndObject(); - } - - private void toJSon(Object dto, JsonGenerator jsonGenerator, Field prop) throws IOException { - if (getLoginUserRole().isAllowedToRead(prop)) { - final String fieldName = prop.getName(); - if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); - } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeNumberField(fieldName, (long) get(dto, prop)); - } else if (String.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); - } else { - throw new NotImplementedException("property type not yet implemented" + prop); - } - } - } - - private Object get(Object dto, Field field) { - try { - field.setAccessible(true); - return field.get(dto); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - private Role getLoginUserRole() { - return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); - } - - private Object invoke(Object dto, Method method) { - try { - return method.invoke(dto); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - } - @JsonComponent public static class UserJsonDeserializer extends JsonDeserializer { From 0b7ebac4726f4a0c67b5676f89b7d5e7df213d84 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 19 Apr 2019 12:08:04 +0200 Subject: [PATCH 11/24] first shot @AccessFor on MembershipDTO --- .../hsadminng/service/dto/MembershipDTO.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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 63956fec..0f4b5dfc 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java @@ -1,7 +1,12 @@ package org.hostsharing.hsadminng.service.dto; -import java.time.LocalDate; -import javax.validation.constraints.*; + +import org.hostsharing.hsadminng.service.accessfilter.AccessFor; +import org.hostsharing.hsadminng.service.accessfilter.Role; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.io.Serializable; +import java.time.LocalDate; import java.util.Objects; import java.util.function.Consumer; @@ -10,22 +15,29 @@ import java.util.function.Consumer; */ public class MembershipDTO implements Serializable { + @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long id; @NotNull + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate documentDate; @NotNull + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate memberFrom; + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate memberUntil; @Size(max = 160) + @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) private String remark; - + // TODO @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) + // @AccessReference(CustomerDTO.class, Role...) private Long customerId; + @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String customerPrefix; public MembershipDTO with( From 998a5a8aa1f6a4f47bea95b72f384a93bb57f13c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 19 Apr 2019 12:08:30 +0200 Subject: [PATCH 12/24] WIP JSonDeserializerWithAccessFilter --- .../JSonDeserializerWithAccessFilter.java | 34 +++++ .../hsadminng/service/dto/CustomerDTO.java | 29 +--- .../service/util/ReflectionUtil.java | 19 +++ ...nDeserializerWithAccessFilterUnitTest.java | 125 ++++++++++++++++++ 4 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java create mode 100644 src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java create mode 100644 src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java new file mode 100644 index 00000000..3ab6f061 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -0,0 +1,34 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; + +import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked; + +public class JSonDeserializerWithAccessFilter { + + private final T dto; + private final TreeNode treeNode; + + public JSonDeserializerWithAccessFilter(final JsonParser jsonParser, final DeserializationContext deserializationContext, Class dtoClass) { + this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser)); + this.dto = unchecked(() -> dtoClass.newInstance()); + } + + public T deserialize() { +// +// CustomerDTO dto = new CustomerDTO(); +// dto.setId(((IntNode) treeNode.get("id")).asLong()); +// dto.setReference(((IntNode) treeNode.get("reference")).asInt()); +// dto.setPrefix(((TextNode) treeNode.get("prefix")).asText()); +// dto.setName(((TextNode) treeNode.get("name")).asText()); +// dto.setContractualAddress(((TextNode) treeNode.get("contractualAddress")).asText()); +// dto.setContractualSalutation(((TextNode) treeNode.get("contractualSalutation")).asText()); +// dto.setBillingAddress(((TextNode) treeNode.get("billingAddress")).asText()); +// dto.setBillingSalutation(((TextNode) treeNode.get("billingSalutation")).asText()); +// dto.setRemark(((TextNode) treeNode.get("remark")).asText()); + + return dto; + } +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index 18b6e793..58abb893 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -1,27 +1,19 @@ package org.hostsharing.hsadminng.service.dto; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.TextNode; -import org.apache.commons.lang3.NotImplementedException; -import org.hostsharing.hsadminng.security.SecurityUtils; import org.hostsharing.hsadminng.service.accessfilter.AccessFor; +import org.hostsharing.hsadminng.service.accessfilter.JSonDeserializerWithAccessFilter; import org.hostsharing.hsadminng.service.accessfilter.Role; import org.springframework.boot.jackson.JsonComponent; import javax.validation.constraints.*; import java.io.IOException; import java.io.Serializable; -import java.lang.annotation.*; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.Objects; /** @@ -182,23 +174,10 @@ public class CustomerDTO implements Serializable { public static class UserJsonDeserializer extends JsonDeserializer { @Override - public CustomerDTO deserialize(JsonParser jsonParser, - DeserializationContext deserializationContext) throws IOException { + public CustomerDTO deserialize(final JsonParser jsonParser, + final DeserializationContext deserializationContext) throws IOException { - TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser); - - CustomerDTO dto = new CustomerDTO(); - dto.setId(((IntNode) treeNode.get("id")).asLong()); - dto.setReference(((IntNode) treeNode.get("reference")).asInt()); - dto.setPrefix(((TextNode) treeNode.get("prefix")).asText()); - dto.setName(((TextNode) treeNode.get("name")).asText()); - dto.setContractualAddress(((TextNode) treeNode.get("contractualAddress")).asText()); - dto.setContractualSalutation(((TextNode) treeNode.get("contractualSalutation")).asText()); - dto.setBillingAddress(((TextNode) treeNode.get("billingAddress")).asText()); - dto.setBillingSalutation(((TextNode) treeNode.get("billingSalutation")).asText()); - dto.setRemark(((TextNode) treeNode.get("remark")).asText()); - - return dto; + return new JSonDeserializerWithAccessFilter(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); } } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java new file mode 100644 index 00000000..ff5c8de3 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java @@ -0,0 +1,19 @@ +package org.hostsharing.hsadminng.service.util; + +import java.util.function.Supplier; + +public class ReflectionUtil { + + @FunctionalInterface + public interface ThrowingSupplier { + T get() throws Exception; + } + + public static T unchecked(final ThrowingSupplier supplier) { + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java new file mode 100644 index 00000000..c0c93a47 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -0,0 +1,125 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import com.fasterxml.jackson.core.JsonGenerator; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class JSonDeserializerWithAccessFilterUnitTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + public JsonGenerator jsonGenerator; + + @Before + public void init() { + givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + } + + @Test + public void shouldDeserializeStringField() throws IOException { + // given + final String givenJSon = asJSon(ImmutablePair.of("stringField", "String Value")); + + // when + new JSonDeserializerWithAccessFilter().deserialize(givenJSon, jsonGenerator, null); + + // then + verify(jsonGenerator).writeStringField("openStringField", givenDTO.openStringField); + } + + @Test + public void shouldSerializeRestrictedFieldIfRequiredRoleIsCoveredByUser() throws IOException { + + // given + givenLoginUserWithRole(Role.FINANCIAL_CONTACT); + + // when + new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + + // then + verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); + } + + @Test + public void shouldNotSerializeRestrictedFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + + // given + givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + + // when + new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + + // then + verify(jsonGenerator, never()).writeStringField("restrictedField", givenDTO.restrictedField); + } + + @Test + public void shouldThrowExceptionForUnimplementedFieldType() throws IOException { + + // given + class Arbitrary { + } + class GivenDtoWithUnimplementedFieldType { + @AccessFor(read = Role.ANYBODY) + Arbitrary fieldWithUnimplementedType; + } + final GivenDtoWithUnimplementedFieldType givenDto = new GivenDtoWithUnimplementedFieldType(); + + // when + Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter().serialize(givenDto, jsonGenerator, null)); + + // then + assertThat(actual).isInstanceOf(NotImplementedException.class); + } + + // --- fixture code below --- + + private String asJSon(final ImmutablePair... properties) { + final StringBuilder json = new StringBuilder(); + for ( ImmutablePair prop: properties ) { + json.append(inQuotes(prop.left)); + json.append(": "); + if ( prop.right instanceof Number ) { + json.append(prop.right); + } else { + json.append(inQuotes(prop.right)); + } + json.append(",\n"); + } + return "{\n" + json.substring(0, json.length()-2) + "\n}"; + } + + private String inQuotes(Object value) { + return "\"" + value.toString() + "\""; + } + + private static class GivenDto { + @AccessFor(update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) + String restrictedField; + + @AccessFor(update = Role.ANYBODY) + String openStringField; + + @AccessFor(update = Role.ANYBODY) + Integer openIntegerField; + + @AccessFor(update = Role.ANYBODY) + Long openLongField; + } +} From 1dae396d99c002fc58ae0a33eda610d491cbc6c7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 06:17:55 +0200 Subject: [PATCH 13/24] JSonDeserializerWithAccessFilter --- package-lock.json | 41 ++++++++--- .../JSonDeserializerWithAccessFilter.java | 53 +++++++++++---- .../JSonSerializerWithAccessFilter.java | 2 +- .../hsadminng/service/dto/CustomerDTO.java | 4 +- .../service/util/ReflectionUtil.java | 22 ++++++ ...nDeserializerWithAccessFilterUnitTest.java | 68 +++++++++---------- ...SonSerializerWithAccessFilterUnitTest.java | 2 +- 7 files changed, 130 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 544d36a9..b13a9116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6025,7 +6025,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6046,12 +6047,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6066,17 +6069,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6193,7 +6199,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6205,6 +6212,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6219,6 +6227,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6226,12 +6235,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6250,6 +6261,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6330,7 +6342,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6342,6 +6355,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6427,7 +6441,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -6463,6 +6478,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6482,6 +6498,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6525,12 +6542,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, 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 3ab6f061..40d02455 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -3,6 +3,13 @@ package org.hostsharing.hsadminng.service.accessfilter; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.apache.commons.lang3.NotImplementedException; +import org.hostsharing.hsadminng.service.util.ReflectionUtil; + +import java.lang.reflect.Field; import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked; @@ -17,18 +24,40 @@ public class JSonDeserializerWithAccessFilter { } public T deserialize() { -// -// CustomerDTO dto = new CustomerDTO(); -// dto.setId(((IntNode) treeNode.get("id")).asLong()); -// dto.setReference(((IntNode) treeNode.get("reference")).asInt()); -// dto.setPrefix(((TextNode) treeNode.get("prefix")).asText()); -// dto.setName(((TextNode) treeNode.get("name")).asText()); -// dto.setContractualAddress(((TextNode) treeNode.get("contractualAddress")).asText()); -// dto.setContractualSalutation(((TextNode) treeNode.get("contractualSalutation")).asText()); -// dto.setBillingAddress(((TextNode) treeNode.get("billingAddress")).asText()); -// dto.setBillingSalutation(((TextNode) treeNode.get("billingSalutation")).asText()); -// dto.setRemark(((TextNode) treeNode.get("remark")).asText()); - + treeNode.fieldNames().forEachRemaining(fieldName -> { + try { + final Field field = dto.getClass().getDeclaredField(fieldName); + final Object value = readValue(treeNode, field); + writeValue(dto, field, value); + } catch (NoSuchFieldException e) { + throw new RuntimeException("setting field " + fieldName + " failed", e); + } + }); return dto; } + + private Object readValue(final TreeNode treeNode, final Field field) { + final TreeNode fieldNode = treeNode.get(field.getName()); + if (fieldNode instanceof TextNode) { + return ((TextNode)fieldNode).asText(); + } else if (fieldNode instanceof IntNode) { + return ((IntNode)fieldNode).asInt(); + } else if (fieldNode instanceof LongNode) { + return ((LongNode)fieldNode).asLong(); + } else { + throw new NotImplementedException("property type not yet implemented: " + field); + } + } + + private void writeValue(final T dto, final Field field, final Object value) { + if ( field.getType().isAssignableFrom(value.getClass()) ) { + ReflectionUtil.setValue(dto, field, value); + } else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) { + ReflectionUtil.setValue(dto, field, ((Number)value).intValue()); + } else if (Long.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) { + ReflectionUtil.setValue(dto, field, ((Number)value).longValue()); + } else { + throw new NotImplementedException("property type not yet implemented: " + field); + } + } } 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 2fd08c51..2e6ce295 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -40,7 +40,7 @@ public class JSonSerializerWithAccessFilter extends JsonSerializer { } else if (String.class.isAssignableFrom(prop.getType())) { jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); } else { - throw new NotImplementedException("property type not yet implemented" + prop); + throw new NotImplementedException("property type not yet implemented: " + prop); } } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index 58abb893..f789c4be 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -175,9 +175,9 @@ public class CustomerDTO implements Serializable { @Override public CustomerDTO deserialize(final JsonParser jsonParser, - final DeserializationContext deserializationContext) throws IOException { + final DeserializationContext deserializationContext) { - return new JSonDeserializerWithAccessFilter(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); + return new JSonDeserializerWithAccessFilter<>(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); } } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java index ff5c8de3..9492a28c 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java +++ b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java @@ -1,9 +1,31 @@ package org.hostsharing.hsadminng.service.util; +import com.fasterxml.jackson.core.TreeNode; + +import java.lang.reflect.Field; import java.util.function.Supplier; public class ReflectionUtil { + public static void setValue(final T dto, final String fieldName, final Object value) { + try { + final Field field = dto.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(dto, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public static void setValue(final T dto, final Field field, final Object value) { + try { + field.setAccessible(true); + field.set(dto, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + @FunctionalInterface public interface ThrowingSupplier { T get() throws Exception; diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index c0c93a47..2e3bcaa4 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -1,8 +1,14 @@ package org.hostsharing.hsadminng.service.accessfilter; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.tuple.ImmutablePair; +import org.hostsharing.hsadminng.service.dto.CustomerDTO; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -15,6 +21,7 @@ import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -24,68 +31,55 @@ public class JSonDeserializerWithAccessFilterUnitTest { public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock - public JsonGenerator jsonGenerator; + public JsonParser jsonParser; + + @Mock + public ObjectCodec codec; + + @Mock + public TreeNode treeNode; @Before - public void init() { + public void init() throws IOException { givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + + given(jsonParser.getCodec()).willReturn(codec); } @Test public void shouldDeserializeStringField() throws IOException { // given - final String givenJSon = asJSon(ImmutablePair.of("stringField", "String Value")); + givenJSonTree(asJSon(ImmutablePair.of("openStringField", "String Value"))); // when - new JSonDeserializerWithAccessFilter().deserialize(givenJSon, jsonGenerator, null); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); // then - verify(jsonGenerator).writeStringField("openStringField", givenDTO.openStringField); + assertThat(actualDto.openStringField).isEqualTo("String Value"); } @Test - public void shouldSerializeRestrictedFieldIfRequiredRoleIsCoveredByUser() throws IOException { - + public void shouldDeserializeIntegerField() throws IOException { // given - givenLoginUserWithRole(Role.FINANCIAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("openIntegerField", 1234))); // when - new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); // then - verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); + assertThat(actualDto.openIntegerField).isEqualTo(1234); } @Test - public void shouldNotSerializeRestrictedFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { - + public void shouldDeserializeLongField() throws IOException { // given - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenJSonTree(asJSon(ImmutablePair.of("openLongField", 1234L))); // when - new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); // then - verify(jsonGenerator, never()).writeStringField("restrictedField", givenDTO.restrictedField); - } - - @Test - public void shouldThrowExceptionForUnimplementedFieldType() throws IOException { - - // given - class Arbitrary { - } - class GivenDtoWithUnimplementedFieldType { - @AccessFor(read = Role.ANYBODY) - Arbitrary fieldWithUnimplementedType; - } - final GivenDtoWithUnimplementedFieldType givenDto = new GivenDtoWithUnimplementedFieldType(); - - // when - Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter().serialize(givenDto, jsonGenerator, null)); - - // then - assertThat(actual).isInstanceOf(NotImplementedException.class); + assertThat(actualDto.openLongField).isEqualTo(1234L); } // --- fixture code below --- @@ -105,11 +99,15 @@ public class JSonDeserializerWithAccessFilterUnitTest { return "{\n" + json.substring(0, json.length()-2) + "\n}"; } + private void givenJSonTree(String givenJSon) throws IOException { + given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); + } + private String inQuotes(Object value) { return "\"" + value.toString() + "\""; } - private static class GivenDto { + public static class GivenDto { @AccessFor(update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) String restrictedField; diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java index 9a5e838f..2e7be2fb 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -36,7 +36,7 @@ public class JSonSerializerWithAccessFilterUnitTest { @Test public void shouldSerializeStringField() throws IOException { - // when + // when new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); // then From 741e91bb784f3dbbdb1609c1bea2402f8d0524bc Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 06:23:03 +0200 Subject: [PATCH 14/24] JSonSerializerWithAccessFilter - idea for generalization of field types --- .../service/accessfilter/JSonSerializerWithAccessFilter.java | 3 +++ 1 file changed, 3 insertions(+) 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 2e6ce295..2d30b251 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -33,6 +33,9 @@ public class JSonSerializerWithAccessFilter extends JsonSerializer { if (getLoginUserRole().isAllowedToRead(prop)) { final String fieldName = prop.getName(); // TODO: maybe replace by serializerProvider.defaultSerialize...()? + // But that's difficult for parallel structure with the deserializer, where the API is ugly. + // 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. if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { From 90316a262bc1d2406374874b841cbc36e0427351 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 06:28:36 +0200 Subject: [PATCH 15/24] JSonSerializerWithAccessFilter - idea for parallel structure with deserializer --- .../service/accessfilter/JSonSerializerWithAccessFilter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 2d30b251..18770a28 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -20,7 +20,9 @@ public class JSonSerializerWithAccessFilter extends JsonSerializer { public void serialize(final Object dto, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException { - // TODO: move the implementation to an (if necessary, inner) class + // TODO: Move the implementation to an (if necessary, inner) class, or maybe better + // expose just the inner implementation from an explicit @JsonCompontent + // as it's necessary for the deserializers anyway. jsonGenerator.writeStartObject(); for (Field prop : dto.getClass().getDeclaredFields()) { toJSon(dto, jsonGenerator, prop); From bb0fb4aa7880515c7d1c3f54bb4e79f3a30def01 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 06:57:06 +0200 Subject: [PATCH 16/24] parallel structure for JSonSerializer/DeserializerWithAccessFilter --- .../JSonDeserializerWithAccessFilter.java | 3 ++- .../JSonSerializerWithAccessFilter.java | 26 ++++++++++++------- .../hsadminng/service/dto/CustomerDTO.java | 21 +++++++++++++-- ...SonSerializerWithAccessFilterUnitTest.java | 17 ++++++------ 4 files changed, 46 insertions(+), 21 deletions(-) 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 40d02455..e9c25ae9 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -20,9 +20,10 @@ public class JSonDeserializerWithAccessFilter { public JSonDeserializerWithAccessFilter(final JsonParser jsonParser, final DeserializationContext deserializationContext, Class dtoClass) { this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser)); - this.dto = unchecked(() -> dtoClass.newInstance()); + this.dto = unchecked(dtoClass::newInstance); } + // Jackson deserializes from the JsonParser, thus no input parameter needed. public T deserialize() { treeNode.fieldNames().forEachRemaining(fieldName -> { try { 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 18770a28..c4d1986a 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -13,21 +13,26 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -@JsonComponent -public class JSonSerializerWithAccessFilter extends JsonSerializer { +public class JSonSerializerWithAccessFilter { + private final JsonGenerator jsonGenerator; + private final SerializerProvider serializerProvider; + private final T dto; - @Override - public void serialize(final Object dto, final JsonGenerator jsonGenerator, - final SerializerProvider serializerProvider) throws IOException { + public JSonSerializerWithAccessFilter(final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider, + final T dto) { + this.jsonGenerator = jsonGenerator; + this.serializerProvider = serializerProvider; + this.dto = dto; + } + + // Jackson serializes into the JsonGenerator, thus no return value needed. + public void serialize() throws IOException { - // TODO: Move the implementation to an (if necessary, inner) class, or maybe better - // expose just the inner implementation from an explicit @JsonCompontent - // as it's necessary for the deserializers anyway. jsonGenerator.writeStartObject(); for (Field prop : dto.getClass().getDeclaredFields()) { toJSon(dto, jsonGenerator, prop); } - jsonGenerator.writeEndObject(); } @@ -35,9 +40,10 @@ public class JSonSerializerWithAccessFilter extends JsonSerializer { if (getLoginUserRole().isAllowedToRead(prop)) { final String fieldName = prop.getName(); // TODO: maybe replace by serializerProvider.defaultSerialize...()? - // But that's difficult for parallel structure with the deserializer, where the API is ugly. + // 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? if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index f789c4be..cdec25f3 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -1,13 +1,17 @@ package org.hostsharing.hsadminng.service.dto; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.TextNode; import org.hostsharing.hsadminng.service.accessfilter.AccessFor; import org.hostsharing.hsadminng.service.accessfilter.JSonDeserializerWithAccessFilter; +import org.hostsharing.hsadminng.service.accessfilter.JSonSerializerWithAccessFilter; import org.hostsharing.hsadminng.service.accessfilter.Role; import org.springframework.boot.jackson.JsonComponent; @@ -171,13 +175,26 @@ public class CustomerDTO implements Serializable { } @JsonComponent - public static class UserJsonDeserializer extends JsonDeserializer { + public static class CustomerJsonSerializer extends JsonSerializer { + + @Override + public void serialize(final CustomerDTO customerDTO, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) throws IOException { + + new JSonSerializerWithAccessFilter<>(jsonGenerator, serializerProvider, customerDTO).serialize(); + } + } + + + + @JsonComponent + public static class CustomerJsonDeserializer extends JsonDeserializer { @Override public CustomerDTO deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) { - return new JSonDeserializerWithAccessFilter<>(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); + return new JSonDeserializerWithAccessFilter<>(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); } } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java index 2e7be2fb..432a53ad 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -37,7 +37,7 @@ public class JSonSerializerWithAccessFilterUnitTest { @Test public void shouldSerializeStringField() throws IOException { // when - new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); // then verify(jsonGenerator).writeStringField("openStringField", givenDTO.openStringField); @@ -50,10 +50,10 @@ public class JSonSerializerWithAccessFilterUnitTest { givenLoginUserWithRole(Role.FINANCIAL_CONTACT); // when - new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); // then - verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); + verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); } @Test @@ -63,25 +63,26 @@ public class JSonSerializerWithAccessFilterUnitTest { givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); // when - new JSonSerializerWithAccessFilter().serialize(givenDTO, jsonGenerator, null); + new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); // then verify(jsonGenerator, never()).writeStringField("restrictedField", givenDTO.restrictedField); } @Test - public void shouldThrowExceptionForUnimplementedFieldType() throws IOException { + public void shouldThrowExceptionForUnimplementedFieldType() { // given - class Arbitrary {} + class Arbitrary { + } class GivenDtoWithUnimplementedFieldType { @AccessFor(read = Role.ANYBODY) Arbitrary fieldWithUnimplementedType; } - final GivenDtoWithUnimplementedFieldType givenDto = new GivenDtoWithUnimplementedFieldType(); + final GivenDtoWithUnimplementedFieldType givenDtoWithUnimplementedFieldType = new GivenDtoWithUnimplementedFieldType(); // when - Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter().serialize(givenDto, jsonGenerator, null)); + Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDtoWithUnimplementedFieldType).serialize()); // then assertThat(actual).isInstanceOf(NotImplementedException.class); From 63bd60239732533f5e829e14df1a42a2dbab8184 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 08:31:26 +0200 Subject: [PATCH 17/24] JSonDeserializerWithAccessFilter with working access rights validation --- .../service/accessfilter/AccessFor.java | 2 +- .../JSonDeserializerWithAccessFilter.java | 69 +++++++++-- .../service/accessfilter/SelfId.java | 17 +++ .../hsadminng/service/dto/CustomerDTO.java | 9 +- .../service/util/ReflectionUtil.java | 9 ++ .../rest/errors/BadRequestAlertException.java | 20 ++-- .../web/rest/errors/ExceptionTranslator.java | 2 +- src/main/webapp/i18n/de/custom-error.json | 4 +- src/main/webapp/i18n/en/custom-error.json | 4 +- ...nDeserializerWithAccessFilterUnitTest.java | 107 +++++++++++++++--- 10 files changed, 198 insertions(+), 45 deletions(-) create mode 100644 src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java index d25b5c87..852f6138 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/AccessFor.java @@ -3,9 +3,9 @@ package org.hostsharing.hsadminng.service.accessfilter; import java.lang.annotation.*; +@Documented @Target({ElementType.FIELD, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) -@Documented public @interface AccessFor { Role[] init() default Role.NOBODY; 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 e9c25ae9..13e3fbdd 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -7,9 +7,13 @@ import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.TextNode; import org.apache.commons.lang3.NotImplementedException; +import org.hostsharing.hsadminng.security.SecurityUtils; import org.hostsharing.hsadminng.service.util.ReflectionUtil; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked; @@ -17,6 +21,8 @@ public class JSonDeserializerWithAccessFilter { private final T dto; private final TreeNode treeNode; + private final Set modifiedFields = new HashSet<>(); + private Field selfIdField = null; public JSonDeserializerWithAccessFilter(final JsonParser jsonParser, final DeserializationContext deserializationContext, Class dtoClass) { this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser)); @@ -30,35 +36,82 @@ public class JSonDeserializerWithAccessFilter { final Field field = dto.getClass().getDeclaredField(fieldName); final Object value = readValue(treeNode, field); writeValue(dto, field, value); + markAsModified(field); } catch (NoSuchFieldException e) { throw new RuntimeException("setting field " + fieldName + " failed", e); } }); + + modifiedFields.forEach(this::checkAccess); return dto; } private Object readValue(final TreeNode treeNode, final Field field) { final TreeNode fieldNode = treeNode.get(field.getName()); if (fieldNode instanceof TextNode) { - return ((TextNode)fieldNode).asText(); + return ((TextNode) fieldNode).asText(); } else if (fieldNode instanceof IntNode) { - return ((IntNode)fieldNode).asInt(); + return ((IntNode) fieldNode).asInt(); } else if (fieldNode instanceof LongNode) { - return ((LongNode)fieldNode).asLong(); + return ((LongNode) fieldNode).asLong(); } else { throw new NotImplementedException("property type not yet implemented: " + field); } } private void writeValue(final T dto, final Field field, final Object value) { - if ( field.getType().isAssignableFrom(value.getClass()) ) { + if (field.getType().isAssignableFrom(value.getClass())) { ReflectionUtil.setValue(dto, field, value); - } else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) { - ReflectionUtil.setValue(dto, field, ((Number)value).intValue()); + } else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) { + ReflectionUtil.setValue(dto, field, ((Number) value).intValue()); } else if (Long.class.isAssignableFrom(field.getType()) || long.class.isAssignableFrom(field.getType())) { - ReflectionUtil.setValue(dto, field, ((Number)value).longValue()); - } else { + ReflectionUtil.setValue(dto, field, ((Number) value).longValue()); + } else { throw new NotImplementedException("property type not yet implemented: " + field); } } + + private void markAsModified(final Field field) { + modifiedFields.add(field); + } + + private Object getId() { + if (selfIdField == null) { + return null; + } + return ReflectionUtil.getValue(dto, selfIdField); + } + + private void checkAccess(final Field field) { + if ( !rememberSelfIdField(field) ) { + if (getId() == null) { + if (!getLoginUserRole().isAllowedToInit(field)) { + throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited"); + } + } else if (getId() != null) { + if (!getLoginUserRole().isAllowedToUpdate(field)) { + throw new BadRequestAlertException("Update of field prohibited for current user", toDisplay(field), "updateProhibited"); + } + } + } + } + + private boolean rememberSelfIdField(final Field field) { + if ( field.isAnnotationPresent(SelfId.class) ) { + if ( selfIdField != null ) { + throw new AssertionError("multiple " + SelfId.class + " detected in " + field.getDeclaringClass().getSimpleName() ); + } + selfIdField = field; + return true; + } + return false; + } + + private String toDisplay(final Field field) { + return field.getDeclaringClass().getSimpleName() + "." + field.getName(); + } + + private Role getLoginUserRole() { + return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); + } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java new file mode 100644 index 00000000..8d3a352d --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java @@ -0,0 +1,17 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import java.lang.annotation.*; + +/** + * Used to mark a field within a DTO as containing the id of a field, + * it's needed to identify an existing entity for update functions. + * Initialization and update rights have no meaning for such fields, + * its initialized automatically and never updated. + * + * @see AccessFor + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SelfId { +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index cdec25f3..28a84d2a 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -2,17 +2,11 @@ package org.hostsharing.hsadminng.service.dto; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.node.IntNode; -import com.fasterxml.jackson.databind.node.TextNode; -import org.hostsharing.hsadminng.service.accessfilter.AccessFor; -import org.hostsharing.hsadminng.service.accessfilter.JSonDeserializerWithAccessFilter; -import org.hostsharing.hsadminng.service.accessfilter.JSonSerializerWithAccessFilter; -import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.accessfilter.*; import org.springframework.boot.jackson.JsonComponent; import javax.validation.constraints.*; @@ -25,6 +19,7 @@ import java.util.Objects; */ public class CustomerDTO implements Serializable { + @SelfId @AccessFor(read = Role.ANY_CUSTOMER_USER) private Long id; diff --git a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java index 9492a28c..0bcabc78 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java +++ b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java @@ -26,6 +26,15 @@ public class ReflectionUtil { } } + public static Object getValue(T dto, Field field) { + try { + field.setAccessible(true); + return field.get(dto); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + @FunctionalInterface public interface ThrowingSupplier { T get() throws Exception; diff --git a/src/main/java/org/hostsharing/hsadminng/web/rest/errors/BadRequestAlertException.java b/src/main/java/org/hostsharing/hsadminng/web/rest/errors/BadRequestAlertException.java index 0548500f..f395377a 100644 --- a/src/main/java/org/hostsharing/hsadminng/web/rest/errors/BadRequestAlertException.java +++ b/src/main/java/org/hostsharing/hsadminng/web/rest/errors/BadRequestAlertException.java @@ -11,32 +11,32 @@ public class BadRequestAlertException extends AbstractThrowableProblem { private static final long serialVersionUID = 1L; - private final String entityName; + private final String param; private final String errorKey; - public BadRequestAlertException(String defaultMessage, String entityName, String errorKey) { - this(ErrorConstants.DEFAULT_TYPE, defaultMessage, entityName, errorKey); + public BadRequestAlertException(String defaultMessage, String param, String errorKey) { + this(ErrorConstants.DEFAULT_TYPE, defaultMessage, param, errorKey); } - public BadRequestAlertException(URI type, String defaultMessage, String entityName, String errorKey) { - super(type, defaultMessage, Status.BAD_REQUEST, null, null, null, getAlertParameters(entityName, errorKey)); - this.entityName = entityName; + public BadRequestAlertException(URI type, String defaultMessage, String param, String errorKey) { + super(type, defaultMessage, Status.BAD_REQUEST, null, null, null, getAlertParameters(param, errorKey)); + this.param = param; this.errorKey = errorKey; } - public String getEntityName() { - return entityName; + public String getParam() { + return param; } public String getErrorKey() { return errorKey; } - private static Map getAlertParameters(String entityName, String errorKey) { + private static Map getAlertParameters(String param, String errorKey) { Map parameters = new HashMap<>(); parameters.put("message", "error." + errorKey); - parameters.put("params", entityName); + parameters.put("params", param); return parameters; } } diff --git a/src/main/java/org/hostsharing/hsadminng/web/rest/errors/ExceptionTranslator.java b/src/main/java/org/hostsharing/hsadminng/web/rest/errors/ExceptionTranslator.java index d810090c..ea414e84 100644 --- a/src/main/java/org/hostsharing/hsadminng/web/rest/errors/ExceptionTranslator.java +++ b/src/main/java/org/hostsharing/hsadminng/web/rest/errors/ExceptionTranslator.java @@ -104,7 +104,7 @@ public class ExceptionTranslator implements ProblemHandling { @ExceptionHandler public ResponseEntity handleBadRequestAlertException(BadRequestAlertException ex, NativeWebRequest request) { - return create(ex, request, HeaderUtil.createFailureAlert(ex.getEntityName(), ex.getErrorKey(), ex.getMessage())); + return create(ex, request, HeaderUtil.createFailureAlert(ex.getParam(), ex.getErrorKey(), ex.getMessage())); } @ExceptionHandler diff --git a/src/main/webapp/i18n/de/custom-error.json b/src/main/webapp/i18n/de/custom-error.json index 11be4363..89c3fffc 100644 --- a/src/main/webapp/i18n/de/custom-error.json +++ b/src/main/webapp/i18n/de/custom-error.json @@ -5,6 +5,8 @@ "shareTransactionImmutable": "Transaktionen mit Geschäftsanteilen sind unveränderlich", "membershipNotDeletable": "Mitgliedschaft kann nicht gelöscht werden, setze stattdessen das 'untilDate'", "untilDateMustBeAfterSinceDate": "Mitgliedshafts-Austrittsdatum muss nach dem Beitrittsdatum liegen", - "anotherUncancelledMembershipExists": "Nur eine einzige ungekündigte Mitgliedschaft pro Kunde ist zulässig" + "anotherUncancelledMembershipExists": "Nur eine einzige ungekündigte Mitgliedschaft pro Kunde ist zulässig", + "initializationProhibited": "Initialisierung des Feldes unzulässig", + "updateProhibited": "Aktualisierung des Feldes unzulässig" } } diff --git a/src/main/webapp/i18n/en/custom-error.json b/src/main/webapp/i18n/en/custom-error.json index 145dfcd1..13d9463a 100644 --- a/src/main/webapp/i18n/en/custom-error.json +++ b/src/main/webapp/i18n/en/custom-error.json @@ -5,6 +5,8 @@ "shareTransactionImmutable": "Share transactions are immutable", "membershipNotDeletable": "Membership cannot be deleted, instead set 'untilDate'", "untilDateMustBeAfterSinceDate": "Membership until date must be after since date", - "anotherUncancelledMembershipExists": "Only a single uncancelled membership allowed per customer" + "anotherUncancelledMembershipExists": "Only a single uncancelled membership allowed per customer", + "initializationProhibited": "Initialization of the field prohibited", + "updateProhibited": "Update of the field prohibited" } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index 2e3bcaa4..2b131490 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -1,14 +1,12 @@ package org.hostsharing.hsadminng.service.accessfilter; -import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.annotation.JsonTypeId; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.TreeNode; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.tuple.ImmutablePair; -import org.hostsharing.hsadminng.service.dto.CustomerDTO; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -22,9 +20,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; +@SuppressWarnings("ALL") public class JSonDeserializerWithAccessFilterUnitTest { @Rule @@ -40,7 +37,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { public TreeNode treeNode; @Before - public void init() throws IOException { + public void init() { givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); given(jsonParser.getCodec()).willReturn(codec); @@ -82,21 +79,81 @@ public class JSonDeserializerWithAccessFilterUnitTest { assertThat(actualDto.openLongField).isEqualTo(1234L); } - // --- fixture code below --- + @Test + public void shouldDeserializeStringFieldIfRequiredRoleIsCoveredByUser() throws IOException { + // given + givenLoginUserWithRole(Role.FINANCIAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); - private String asJSon(final ImmutablePair... properties) { + // when + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); + + // then + assertThat(actualDto.restrictedField).isEqualTo("Restricted String Value"); + } + + @Test + public void shouldInitializeFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + // given + givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); + + // when + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize()); + + // then + assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { + assertThat(badRequestAlertException.getParam()).isEqualTo("GivenDto.restrictedField"); + assertThat(badRequestAlertException.getErrorKey()).isEqualTo("initializationProhibited"); + }); + } + + @Test + public void shouldUpdateFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + // given + givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenJSonTree(asJSon( + ImmutablePair.of("id", 1234L), + ImmutablePair.of("restrictedField", "Restricted String Value"))); + + // when + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize()); + + // then + assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { + assertThat(badRequestAlertException.getParam()).isEqualTo("GivenDto.restrictedField"); + assertThat(badRequestAlertException.getErrorKey()).isEqualTo("updateProhibited"); + }); + } + + @Test + public void should() throws IOException { + // given + givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); + + // when + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDtoWithMultipleSelfId.class).deserialize()); + + // then + assertThat(exception).isInstanceOf(AssertionError.class).hasMessageContaining("xx"); + } + + // --- only fixture code below --- + + @SafeVarargs + private final String asJSon(final ImmutablePair... properties) { final StringBuilder json = new StringBuilder(); - for ( ImmutablePair prop: properties ) { + for (ImmutablePair prop : properties) { json.append(inQuotes(prop.left)); json.append(": "); - if ( prop.right instanceof Number ) { + if (prop.right instanceof Number) { json.append(prop.right); } else { json.append(inQuotes(prop.right)); } json.append(",\n"); } - return "{\n" + json.substring(0, json.length()-2) + "\n}"; + return "{\n" + json.substring(0, json.length() - 2) + "\n}"; } private void givenJSonTree(String givenJSon) throws IOException { @@ -108,16 +165,34 @@ public class JSonDeserializerWithAccessFilterUnitTest { } public static class GivenDto { - @AccessFor(update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) + + @SelfId + @AccessFor(read = Role.ANY_CUSTOMER_USER) + Long id; + + @AccessFor(init = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}, update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) String restrictedField; - @AccessFor(update = Role.ANYBODY) + @AccessFor(init = Role.ANYBODY, update = Role.ANYBODY) String openStringField; - @AccessFor(update = Role.ANYBODY) + @AccessFor(init = Role.ANYBODY, update = Role.ANYBODY) Integer openIntegerField; - @AccessFor(update = Role.ANYBODY) + @AccessFor(init = Role.ANYBODY, update = Role.ANYBODY) Long openLongField; } + + + public static class GivenDtoWithMultipleSelfId { + + @SelfId + @AccessFor(read = Role.ANY_CUSTOMER_USER) + Long id; + + @SelfId + @AccessFor(read = Role.ANY_CUSTOMER_USER) + Long id2; + + } } From d2b0f477f2046e8d7b6cebc9da430bdec7f3124e Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 08:42:47 +0200 Subject: [PATCH 18/24] CustomerDTO: AccessFor/Rights-Configuration --- .../service/dto/CustomerDTOUnitTest.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index 7686c234..1379f574 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -31,8 +31,8 @@ public class CustomerDTOUnitTest { public void testSerializationAsContractualCustomerContact() throws JsonProcessingException { // given - CustomerDTO given = createSomeCustomerDTO(); givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + CustomerDTO given = createSomeCustomerDTO(); // when String actual = objectMapper.writeValueAsString(given); @@ -50,8 +50,8 @@ public class CustomerDTOUnitTest { public void testSerializationAsSupporter() throws JsonProcessingException { // given - CustomerDTO given = createSomeCustomerDTO(); givenLoginUserWithRole(Role.SUPPORTER); + CustomerDTO given = createSomeCustomerDTO(); // when String actual = objectMapper.writeValueAsString(given); @@ -63,8 +63,8 @@ public class CustomerDTOUnitTest { @Test public void testDeserializeAsContractualCustomerContact() throws IOException { // given - String json = "{\"id\":1234,\"reference\":10001,\"prefix\":\"abc\",\"name\":\"Mein Name\",\"contractualAddress\":\"Eine Adresse\",\"contractualSalutation\":\"Hallo\",\"billingAddress\":\"Noch eine Adresse\",\"billingSalutation\":\"Moin\",\"remark\":\"Eine Bemerkung\"}"; givenLoginUserWithRole(Role.CONTRACTUAL_CONTACT); + String json = "{\"id\":1234,\"contractualSalutation\":\"Hallo Updated\",\"billingSalutation\":\"Moin Updated\"}"; // when CustomerDTO actual = objectMapper.readValue(json, CustomerDTO.class); @@ -72,15 +72,9 @@ public class CustomerDTOUnitTest { // then CustomerDTO expected = new CustomerDTO(); expected.setId(1234L); - expected.setReference(10001); - expected.setPrefix("abc"); - expected.setName("Mein Name"); - expected.setContractualAddress(null); // not allowed - expected.setContractualSalutation("Hallo"); - expected.setBillingAddress("Noch eine Adresse"); - expected.setBillingSalutation("Moin"); - expected.setRemark("Eine Bemerkung"); - assertEquals(actual, expected); + expected.setContractualSalutation("Hallo Updated"); + expected.setBillingSalutation("Moin Updated"); + assertThat(actual).isEqualToComparingFieldByField(expected); } private String createExpectedJSon(CustomerDTO dto) { From 1505e7bd66b17e95f90ae9db5c054d8dad6e1860 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 09:22:23 +0200 Subject: [PATCH 19/24] JSonDeserializerWithAccessFilter: shouldDetectMultipleSelfIdFields --- .../JSonDeserializerWithAccessFilter.java | 54 ++++++++++--------- .../service/accessfilter/SelfId.java | 2 +- ...nDeserializerWithAccessFilterUnitTest.java | 7 ++- 3 files changed, 34 insertions(+), 29 deletions(-) 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 13e3fbdd..dc9f2f62 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -31,6 +31,24 @@ public class JSonDeserializerWithAccessFilter { // Jackson deserializes from the JsonParser, thus no input parameter needed. public T deserialize() { + determineSelfIdField(); + deserializeValues(); + checkAccessToModifiedFields(); + return dto; + } + + private void determineSelfIdField() { + for (Field field : dto.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(SelfId.class)) { + if (selfIdField != null) { + throw new AssertionError("multiple @" + SelfId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName()); + } + selfIdField = field; + } + } + } + + private void deserializeValues() { treeNode.fieldNames().forEachRemaining(fieldName -> { try { final Field field = dto.getClass().getDeclaredField(fieldName); @@ -41,9 +59,6 @@ public class JSonDeserializerWithAccessFilter { throw new RuntimeException("setting field " + fieldName + " failed", e); } }); - - modifiedFields.forEach(this::checkAccess); - return dto; } private Object readValue(final TreeNode treeNode, final Field field) { @@ -82,29 +97,20 @@ public class JSonDeserializerWithAccessFilter { return ReflectionUtil.getValue(dto, selfIdField); } - private void checkAccess(final Field field) { - if ( !rememberSelfIdField(field) ) { - if (getId() == null) { - if (!getLoginUserRole().isAllowedToInit(field)) { - throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited"); - } - } else if (getId() != null) { - if (!getLoginUserRole().isAllowedToUpdate(field)) { - throw new BadRequestAlertException("Update of field prohibited for current user", toDisplay(field), "updateProhibited"); + private void checkAccessToModifiedFields() { + modifiedFields.forEach(field -> { + if ( !field.equals(selfIdField) ) { + if (getId() == null) { + if (!getLoginUserRole().isAllowedToInit(field)) { + throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited"); + } + } else if (getId() != null) { + if (!getLoginUserRole().isAllowedToUpdate(field)) { + throw new BadRequestAlertException("Update of field prohibited for current user", toDisplay(field), "updateProhibited"); + } } } - } - } - - private boolean rememberSelfIdField(final Field field) { - if ( field.isAnnotationPresent(SelfId.class) ) { - if ( selfIdField != null ) { - throw new AssertionError("multiple " + SelfId.class + " detected in " + field.getDeclaringClass().getSimpleName() ); - } - selfIdField = field; - return true; - } - return false; + }); } private String toDisplay(final Field field) { diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java index 8d3a352d..baf95906 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java @@ -3,7 +3,7 @@ package org.hostsharing.hsadminng.service.accessfilter; import java.lang.annotation.*; /** - * Used to mark a field within a DTO as containing the id of a field, + * Used to mark a field within a DTO as containing the own id, * it's needed to identify an existing entity for update functions. * Initialization and update rights have no meaning for such fields, * its initialized automatically and never updated. diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index 2b131490..9ddafa09 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -127,15 +127,15 @@ public class JSonDeserializerWithAccessFilterUnitTest { } @Test - public void should() throws IOException { + public void shouldDetectMultipleSelfIdFields() throws IOException { // given - givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); + givenJSonTree(asJSon(ImmutablePair.of("id", 1111L))); // when Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDtoWithMultipleSelfId.class).deserialize()); // then - assertThat(exception).isInstanceOf(AssertionError.class).hasMessageContaining("xx"); + assertThat(exception).isInstanceOf(AssertionError.class).hasMessage("multiple @SelfId detected in GivenDtoWithMultipleSelfId"); } // --- only fixture code below --- @@ -183,7 +183,6 @@ public class JSonDeserializerWithAccessFilterUnitTest { Long openLongField; } - public static class GivenDtoWithMultipleSelfId { @SelfId From a94516b3ceed2a05f61779538ae025475064363d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2019 14:37:49 +0200 Subject: [PATCH 20/24] JSonSerializer/DeserializerWithAccessFilter: also use role in parent --- .../hsadminng/security/SecurityUtils.java | 43 ++++++++++ .../accessfilter/JSonAccessFilter.java | 65 +++++++++++++++ .../JSonDeserializerWithAccessFilter.java | 44 ++--------- .../JSonSerializerWithAccessFilter.java | 13 +-- .../service/accessfilter/ParentId.java | 17 ++++ .../hsadminng/service/accessfilter/Role.java | 13 +-- .../hsadminng/service/dto/MembershipDTO.java | 7 +- .../service/util/ReflectionUtil.java | 7 +- ...nDeserializerWithAccessFilterUnitTest.java | 79 ++++++++++++++++--- ...SonSerializerWithAccessFilterUnitTest.java | 19 ++++- .../accessfilter/MockSecurityContext.java | 16 ++-- .../service/dto/CustomerDTOUnitTest.java | 19 ++--- 12 files changed, 247 insertions(+), 95 deletions(-) create mode 100644 src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java create mode 100644 src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java diff --git a/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java b/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java index eb655672..e06fe195 100644 --- a/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java +++ b/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java @@ -1,9 +1,12 @@ package org.hostsharing.hsadminng.security; +import org.hostsharing.hsadminng.service.accessfilter.Role; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; /** @@ -11,6 +14,8 @@ import java.util.Optional; */ public final class SecurityUtils { + private static List userRoleAssignments = new ArrayList<>(); + private SecurityUtils() { } @@ -73,4 +78,42 @@ public final class SecurityUtils { .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals(authority))) .orElse(false); } + + public static Role getLoginUserRoleFor(final Class onDtoClass, final Long onId) { + + final Role highestRole = userRoleAssignments.stream(). + map(ura -> + matches(onDtoClass, onId, ura) + ? ura.role + : Role.ANYBODY). + reduce(Role.ANYBODY, (r1, r2) -> r1.covers(r2) ? r1 : r2); + return highestRole; + } + + private static boolean matches(Class onDtoClass, Long onId, UserRoleAssignment ura) { + final boolean matches = (ura.onClass == null || onDtoClass == ura.onClass) && (ura.onId == null || onId.equals(ura.onId) ); + return matches; + } + + // TODO: depends on https://plan.hostsharing.net/project/hsadmin/us/67?milestone=34 + public static void addUserRole(final Class onClass, final Long onId, final Role role) { + userRoleAssignments.add(new UserRoleAssignment(onClass, onId, role)); + + } + + public static void clearUserRoles() { + userRoleAssignments.clear(); + } + + private static class UserRoleAssignment { + final Class onClass; + final Long onId; + final Role role; + + UserRoleAssignment(Class onClass, Long onId, Role role) { + this.onClass = onClass; + this.onId = onId; + this.role = role; + } + } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java new file mode 100644 index 00000000..c6faaf04 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java @@ -0,0 +1,65 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import org.hostsharing.hsadminng.security.SecurityUtils; +import org.hostsharing.hsadminng.service.util.ReflectionUtil; + +import java.lang.reflect.Field; + +abstract class JSonAccessFilter { + final T dto; + Field selfIdField = null; + Field parentIdField = null; + + JSonAccessFilter(final T dto) { + this.dto = dto; + determineIdFields(); + } + + void determineIdFields() { + for (Field field : dto.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(SelfId.class)) { + if (selfIdField != null) { + throw new AssertionError("multiple @" + SelfId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName()); + } + selfIdField = field; + } + if (field.isAnnotationPresent(ParentId.class)) { + if (parentIdField != null) { + throw new AssertionError("multiple @" + ParentId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName()); + } + parentIdField = field; + } + } + } + + Long getId() { + if (selfIdField == null) { + return null; + } + return (Long) ReflectionUtil.getValue(dto, selfIdField); + } + + String toDisplay(final Field field) { + return field.getDeclaringClass().getSimpleName() + "." + field.getName(); + } + + Role getLoginUserRole() { + final Role roleOnSelf = getLoginUserRoleOnSelf(); + final Role roleOnParent = getLoginUserRoleOnParent(); + return roleOnSelf.covers(roleOnParent) ? roleOnSelf : roleOnParent; + } + + + private Role getLoginUserRoleOnSelf() { + // TODO: find broadest role in self and recursively in parent + return SecurityUtils.getLoginUserRoleFor(dto.getClass(), getId() ); + } + + private Role getLoginUserRoleOnParent() { + if ( parentIdField == null ) { + return Role.ANYBODY; + } + final ParentId parentId = parentIdField.getAnnotation(ParentId.class); + return SecurityUtils.getLoginUserRoleFor(parentId.value(), (Long) ReflectionUtil.getValue(dto, parentIdField) ); + } +} 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 dc9f2f62..9dc104d1 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.TextNode; import org.apache.commons.lang3.NotImplementedException; -import org.hostsharing.hsadminng.security.SecurityUtils; import org.hostsharing.hsadminng.service.util.ReflectionUtil; import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; @@ -17,37 +16,23 @@ import java.util.Set; import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked; -public class JSonDeserializerWithAccessFilter { +public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { - private final T dto; private final TreeNode treeNode; private final Set modifiedFields = new HashSet<>(); - private Field selfIdField = null; public JSonDeserializerWithAccessFilter(final JsonParser jsonParser, final DeserializationContext deserializationContext, Class dtoClass) { + super(unchecked(dtoClass::newInstance)); this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser)); - this.dto = unchecked(dtoClass::newInstance); } // Jackson deserializes from the JsonParser, thus no input parameter needed. public T deserialize() { - determineSelfIdField(); deserializeValues(); checkAccessToModifiedFields(); return dto; } - private void determineSelfIdField() { - for (Field field : dto.getClass().getDeclaredFields()) { - if (field.isAnnotationPresent(SelfId.class)) { - if (selfIdField != null) { - throw new AssertionError("multiple @" + SelfId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName()); - } - selfIdField = field; - } - } - } - private void deserializeValues() { treeNode.fieldNames().forEachRemaining(fieldName -> { try { @@ -90,34 +75,21 @@ public class JSonDeserializerWithAccessFilter { modifiedFields.add(field); } - private Object getId() { - if (selfIdField == null) { - return null; - } - return ReflectionUtil.getValue(dto, selfIdField); - } - private void checkAccessToModifiedFields() { modifiedFields.forEach(field -> { if ( !field.equals(selfIdField) ) { if (getId() == null) { if (!getLoginUserRole().isAllowedToInit(field)) { - throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited"); + if ( !field.equals(parentIdField)) { + throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited"); + } else { + throw new BadRequestAlertException("Referencing field prohibited for current user", toDisplay(field), "referencingProhibited"); + } } - } else if (getId() != null) { - if (!getLoginUserRole().isAllowedToUpdate(field)) { + } else if (!getLoginUserRole().isAllowedToUpdate(field)) { throw new BadRequestAlertException("Update of field prohibited for current user", toDisplay(field), "updateProhibited"); - } } } }); } - - private String toDisplay(final Field field) { - return field.getDeclaringClass().getSimpleName() + "." + field.getName(); - } - - private Role getLoginUserRole() { - return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); - } } 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 c4d1986a..b2ec0c57 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -2,28 +2,22 @@ package org.hostsharing.hsadminng.service.accessfilter; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.apache.commons.lang3.NotImplementedException; -import org.hostsharing.hsadminng.security.SecurityUtils; -import org.springframework.boot.jackson.JsonComponent; import java.io.IOException; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -public class JSonSerializerWithAccessFilter { +public class JSonSerializerWithAccessFilter extends JSonAccessFilter { private final JsonGenerator jsonGenerator; private final SerializerProvider serializerProvider; - private final T dto; public JSonSerializerWithAccessFilter(final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider, final T dto) { + super(dto); this.jsonGenerator = jsonGenerator; this.serializerProvider = serializerProvider; - this.dto = dto; } // Jackson serializes into the JsonGenerator, thus no return value needed. @@ -65,7 +59,4 @@ public class JSonSerializerWithAccessFilter { } } - private Role getLoginUserRole() { - return SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); - } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java new file mode 100644 index 00000000..51ddc0a2 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java @@ -0,0 +1,17 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import java.lang.annotation.*; + +/** + * Used to mark a field within a DTO as containing the id of a referenced entity, + * it's needed to determine access rights for entity creation. + * + * @see AccessFor + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ParentId { + /// The DTO class of the referenced entity. + Class value(); +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java index 81ff1078..83f667d0 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java @@ -1,7 +1,5 @@ package org.hostsharing.hsadminng.service.accessfilter; -import org.hostsharing.hsadminng.security.SecurityUtils; - import java.lang.reflect.Field; /** @@ -52,7 +50,7 @@ public enum Role { */ FINANCIAL_CONTACT(22) { @Override - boolean covers(final Role role) { + public boolean covers(final Role role) { if (role == ACTUAL_CUSTOMER_USER) { return false; } @@ -94,14 +92,15 @@ public enum Role { * * Where 'this' means the Java instance itself as a role of a system user. * - * @example + * {@code * Role.HOSTMASTER.covers(Role.ANY_CUSTOMER_USER) == true + * } * * @param role The required role for a resource. * * @return whether this role comprises the given role */ - boolean covers(final Role role) { + public boolean covers(final Role role) { return this == role || this.level < role.level; } @@ -131,8 +130,6 @@ public enum Role { */ public boolean isAllowedToUpdate(final Field field) { - final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); - final AccessFor accessFor = field.getAnnotation(AccessFor.class); if (accessFor == null) { return false; @@ -150,8 +147,6 @@ public enum Role { */ public boolean isAllowedToRead(final Field field) { - final Role loginUserRole = SecurityUtils.getCurrentUserLogin().map(u -> Role.valueOf(u.toUpperCase())).orElse(Role.ANYBODY); - final AccessFor accessFor = field.getAnnotation(AccessFor.class); if (accessFor == null) { return false; 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 0f4b5dfc..15130c3c 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java @@ -1,7 +1,9 @@ package org.hostsharing.hsadminng.service.dto; import org.hostsharing.hsadminng.service.accessfilter.AccessFor; +import org.hostsharing.hsadminng.service.accessfilter.ParentId; import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.accessfilter.SelfId; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -15,6 +17,7 @@ import java.util.function.Consumer; */ public class MembershipDTO implements Serializable { + @SelfId @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long id; @@ -33,8 +36,8 @@ public class MembershipDTO implements Serializable { @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) private String remark; - // TODO @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) - // @AccessReference(CustomerDTO.class, Role...) + @ParentId(CustomerDTO.class) + @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) private Long customerId; @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) diff --git a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java index 0bcabc78..377f2748 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java +++ b/src/main/java/org/hostsharing/hsadminng/service/util/ReflectionUtil.java @@ -1,9 +1,6 @@ package org.hostsharing.hsadminng.service.util; -import com.fasterxml.jackson.core.TreeNode; - import java.lang.reflect.Field; -import java.util.function.Supplier; public class ReflectionUtil { @@ -26,10 +23,10 @@ public class ReflectionUtil { } } - public static Object getValue(T dto, Field field) { + public static T getValue(final T dto, final Field field) { try { field.setAccessible(true); - return field.get(dto); + return (T) field.get(dto); } catch (IllegalAccessException e) { throw new RuntimeException(e); } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index 9ddafa09..ed3fa7fb 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -1,6 +1,5 @@ package org.hostsharing.hsadminng.service.accessfilter; -import com.fasterxml.jackson.annotation.JsonTypeId; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.TreeNode; @@ -18,7 +17,8 @@ import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; import static org.mockito.BDDMockito.given; @SuppressWarnings("ALL") @@ -38,7 +38,8 @@ public class JSonDeserializerWithAccessFilterUnitTest { @Before public void init() { - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenAuthenticatedUser(); + givenUserHavingRole(GivenDto.class, 1234L, Role.ACTUAL_CUSTOMER_USER); given(jsonParser.getCodec()).willReturn(codec); } @@ -46,7 +47,9 @@ public class JSonDeserializerWithAccessFilterUnitTest { @Test public void shouldDeserializeStringField() throws IOException { // given - givenJSonTree(asJSon(ImmutablePair.of("openStringField", "String Value"))); + givenJSonTree(asJSon( + ImmutablePair.of("id", 1234L), + ImmutablePair.of("openStringField", "String Value"))); // when GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); @@ -58,7 +61,9 @@ public class JSonDeserializerWithAccessFilterUnitTest { @Test public void shouldDeserializeIntegerField() throws IOException { // given - givenJSonTree(asJSon(ImmutablePair.of("openIntegerField", 1234))); + givenJSonTree(asJSon( + ImmutablePair.of("id", 1234L), + ImmutablePair.of("openIntegerField", 1234))); // when GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); @@ -70,7 +75,9 @@ public class JSonDeserializerWithAccessFilterUnitTest { @Test public void shouldDeserializeLongField() throws IOException { // given - givenJSonTree(asJSon(ImmutablePair.of("openLongField", 1234L))); + givenJSonTree(asJSon( + ImmutablePair.of("id", 1234L), + ImmutablePair.of("openLongField", 1234L))); // when GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); @@ -82,8 +89,11 @@ public class JSonDeserializerWithAccessFilterUnitTest { @Test public void shouldDeserializeStringFieldIfRequiredRoleIsCoveredByUser() throws IOException { // given - givenLoginUserWithRole(Role.FINANCIAL_CONTACT); - givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); + givenAuthenticatedUser(); + givenUserHavingRole(GivenDto.class, 1234L, Role.FINANCIAL_CONTACT); + givenJSonTree(asJSon( + ImmutablePair.of("id", 1234L), + ImmutablePair.of("restrictedField", "Restricted String Value"))); // when GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); @@ -95,7 +105,8 @@ public class JSonDeserializerWithAccessFilterUnitTest { @Test public void shouldInitializeFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { // given - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenAuthenticatedUser(); + givenUserHavingRole(null, null, Role.ANY_CUSTOMER_USER); givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); // when @@ -109,9 +120,41 @@ public class JSonDeserializerWithAccessFilterUnitTest { } @Test - public void shouldUpdateFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + public void shouldNotCreateIfRoleRequiredByParentEntityIsNotCoveredByUser() throws IOException { // given - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenAuthenticatedUser(); + givenUserHavingRole(GivenDto.class, 9999L, Role.CONTRACTUAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("parentId", 1111L))); + + // when + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenChildDto.class).deserialize()); + + // then + assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { + assertThat(badRequestAlertException.getParam()).isEqualTo("GivenChildDto.parentId"); + assertThat(badRequestAlertException.getErrorKey()).isEqualTo("referencingProhibited"); + }); + } + + @Test + public void shouldCreateIfRoleRequiredByReferencedEntityIsCoveredByUser() throws IOException { + // given + givenAuthenticatedUser(); + givenUserHavingRole(GivenDto.class, 1111L, Role.CONTRACTUAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("parentId", 1111L))); + + // when + final GivenChildDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenChildDto.class).deserialize(); + + // then + assertThat(actualDto.parentId).isEqualTo(1111L); + } + + @Test + public void shouldNotUpdateFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + // given + givenAuthenticatedUser(); + givenUserHavingRole(GivenDto.class, 1234L, Role.ANY_CUSTOMER_USER); givenJSonTree(asJSon( ImmutablePair.of("id", 1234L), ImmutablePair.of("restrictedField", "Restricted String Value"))); @@ -183,6 +226,20 @@ public class JSonDeserializerWithAccessFilterUnitTest { Long openLongField; } + public static class GivenChildDto { + + @SelfId + @AccessFor(read = Role.ANY_CUSTOMER_USER) + Long id; + + @AccessFor(init = Role.CONTRACTUAL_CONTACT, update = Role.CONTRACTUAL_CONTACT, read = Role.ACTUAL_CUSTOMER_USER) + @ParentId(GivenDto.class) + Long parentId; + + @AccessFor(init = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}, update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) + String restrictedField; + } + public static class GivenDtoWithMultipleSelfId { @SelfId diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java index 432a53ad..7d5265fc 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -15,7 +15,6 @@ import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -31,7 +30,8 @@ public class JSonSerializerWithAccessFilterUnitTest { @Before public void init() { - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + MockSecurityContext.givenAuthenticatedUser(); + MockSecurityContext.givenUserHavingRole(GivenCustomerDto.class, 888L, Role.ANY_CUSTOMER_USER); } @Test @@ -47,7 +47,8 @@ public class JSonSerializerWithAccessFilterUnitTest { public void shouldSerializeRestrictedFieldIfRequiredRoleIsCoveredByUser() throws IOException { // given - givenLoginUserWithRole(Role.FINANCIAL_CONTACT); + MockSecurityContext.givenAuthenticatedUser(); + MockSecurityContext.givenUserHavingRole(GivenCustomerDto.class, 888L, Role.FINANCIAL_CONTACT); // when new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); @@ -60,7 +61,8 @@ public class JSonSerializerWithAccessFilterUnitTest { public void shouldNotSerializeRestrictedFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { // given - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + MockSecurityContext.givenAuthenticatedUser(); + MockSecurityContext.givenUserHavingRole(GivenCustomerDto.class, 888L, Role.ANY_CUSTOMER_USER); // when new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); @@ -92,6 +94,7 @@ public class JSonSerializerWithAccessFilterUnitTest { private GivenDto createSampleDto() { final GivenDto dto = new GivenDto(); + dto.customerId = 888L; dto.restrictedField = RandomStringUtils.randomAlphabetic(10); dto.openStringField = RandomStringUtils.randomAlphabetic(10); dto.openIntegerField = RandomUtils.nextInt(); @@ -99,7 +102,15 @@ public class JSonSerializerWithAccessFilterUnitTest { return dto; } + private static class GivenCustomerDto { + + } + private static class GivenDto { + + @ParentId(GivenCustomerDto.class) + Long customerId; + @AccessFor(read = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) String restrictedField; diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java index 5a656c77..5b3f78b2 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java @@ -5,20 +5,20 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import java.util.Optional; - import static org.assertj.core.api.Assertions.assertThat; public class MockSecurityContext { - public static void givenLoginUserWithRole(final Role userRole) { - final String fakeUserName = userRole.name(); - + public static void givenAuthenticatedUser() { SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); - securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(fakeUserName, "dummy")); + securityContext.setAuthentication(new UsernamePasswordAuthenticationToken("dummyUser", "dummyPassword")); SecurityContextHolder.setContext(securityContext); - Optional login = SecurityUtils.getCurrentUserLogin(); + SecurityUtils.clearUserRoles(); - assertThat(login).describedAs("precondition failed").contains(fakeUserName); + assertThat(SecurityUtils.getCurrentUserLogin()).describedAs("precondition failed").hasValue("dummyUser"); + } + + public static void givenUserHavingRole(final Class onClass, final Long onId, final Role role) { + SecurityUtils.addUserRole(onClass, onId, role); } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index 1379f574..34a016ae 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -2,22 +2,18 @@ package org.hostsharing.hsadminng.service.dto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.hostsharing.hsadminng.security.SecurityUtils; import org.hostsharing.hsadminng.service.accessfilter.Role; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.json.JsonTest; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; -import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenLoginUserWithRole; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; import static org.junit.Assert.assertEquals; @JsonTest @@ -31,7 +27,8 @@ public class CustomerDTOUnitTest { public void testSerializationAsContractualCustomerContact() throws JsonProcessingException { // given - givenLoginUserWithRole(Role.ANY_CUSTOMER_USER); + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, null, Role.ANY_CUSTOMER_USER); CustomerDTO given = createSomeCustomerDTO(); // when @@ -50,7 +47,8 @@ public class CustomerDTOUnitTest { public void testSerializationAsSupporter() throws JsonProcessingException { // given - givenLoginUserWithRole(Role.SUPPORTER); + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, null, Role.SUPPORTER); CustomerDTO given = createSomeCustomerDTO(); // when @@ -63,7 +61,8 @@ public class CustomerDTOUnitTest { @Test public void testDeserializeAsContractualCustomerContact() throws IOException { // given - givenLoginUserWithRole(Role.CONTRACTUAL_CONTACT); + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, null, Role.CONTRACTUAL_CONTACT); String json = "{\"id\":1234,\"contractualSalutation\":\"Hallo Updated\",\"billingSalutation\":\"Moin Updated\"}"; // when @@ -77,6 +76,8 @@ public class CustomerDTOUnitTest { assertThat(actual).isEqualToComparingFieldByField(expected); } + // --- only test fixture below --- + private String createExpectedJSon(CustomerDTO dto) { String json = // the fields in alphanumeric order: toJSonFieldDefinitionIfPresent("id", dto.getId()) + From 639ea062431ad2e08b83904741612005196eea05 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 24 Apr 2019 12:30:26 +0200 Subject: [PATCH 21/24] JSonAccessFilter with initially working (hardcoded) grand parent role --- .../hsadminng/domain/Customer.java | 14 +- .../hsadminng/domain/Membership.java | 16 +- .../hsadminng/service/CustomerService.java | 3 +- .../hsadminng/service/DtoLoader.java | 7 + .../hsadminng/service/MembershipService.java | 3 +- .../accessfilter/JSonAccessFilter.java | 93 +++++++--- .../JSonDeserializerWithAccessFilter.java | 5 +- .../JSonSerializerWithAccessFilter.java | 11 +- .../hsadminng/service/accessfilter/Role.java | 21 ++- .../hsadminng/service/dto/CustomerDTO.java | 19 +- .../hsadminng/service/dto/MembershipDTO.java | 4 +- .../hsadminng/service/dto/ShareDTO.java | 26 ++- .../service/accessfilter/JSonBuilder.java | 27 +++ ...nDeserializerWithAccessFilterUnitTest.java | 43 ++--- ...SonSerializerWithAccessFilterUnitTest.java | 12 +- .../accessfilter/MockSecurityContext.java | 3 + .../service/dto/MembershipDTOUnitTest.java | 85 +++++++++ .../service/dto/ShareDTOUnitTest.java | 163 ++++++++++++++++++ 18 files changed, 468 insertions(+), 87 deletions(-) create mode 100644 src/main/java/org/hostsharing/hsadminng/service/DtoLoader.java create mode 100644 src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java create mode 100644 src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java create mode 100644 src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java diff --git a/src/main/java/org/hostsharing/hsadminng/domain/Customer.java b/src/main/java/org/hostsharing/hsadminng/domain/Customer.java index c7cb1495..8f5aa792 100644 --- a/src/main/java/org/hostsharing/hsadminng/domain/Customer.java +++ b/src/main/java/org/hostsharing/hsadminng/domain/Customer.java @@ -1,15 +1,12 @@ package org.hostsharing.hsadminng.domain; -import com.fasterxml.jackson.annotation.JsonIgnore; - import javax.persistence.*; import javax.validation.constraints.*; - import java.io.Serializable; import java.util.HashSet; -import java.util.Set; import java.util.Objects; +import java.util.Set; /** * A Customer. @@ -65,13 +62,21 @@ public class Customer implements Serializable { @OneToMany(mappedBy = "customer") private Set memberships = new HashSet<>(); + @OneToMany(mappedBy = "customer") private Set sepamandates = new HashSet<>(); + // jhipster-needle-entity-add-field - JHipster will add fields here, do not remove + public Long getId() { return id; } + public Customer id(long id) { + this.id = id; + return this; + } + public void setId(Long id) { this.id = id; } @@ -229,6 +234,7 @@ public class Customer implements Serializable { public void setSepamandates(Set sepaMandates) { this.sepamandates = sepaMandates; } + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here, do not remove @Override diff --git a/src/main/java/org/hostsharing/hsadminng/domain/Membership.java b/src/main/java/org/hostsharing/hsadminng/domain/Membership.java index d6e4e5bd..91749ff7 100644 --- a/src/main/java/org/hostsharing/hsadminng/domain/Membership.java +++ b/src/main/java/org/hostsharing/hsadminng/domain/Membership.java @@ -1,17 +1,16 @@ package org.hostsharing.hsadminng.domain; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import javax.persistence.*; -import javax.validation.constraints.*; - +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.io.Serializable; import java.time.LocalDate; import java.util.HashSet; -import java.util.Set; import java.util.Objects; +import java.util.Set; /** * A Membership. @@ -46,18 +45,26 @@ public class Membership implements Serializable { @OneToMany(mappedBy = "membership") private Set shares = new HashSet<>(); + @OneToMany(mappedBy = "membership") private Set assets = new HashSet<>(); + @ManyToOne(optional = false) @NotNull @JsonIgnoreProperties("memberships") private Customer customer; // jhipster-needle-entity-add-field - JHipster will add fields here, do not remove + public Long getId() { return id; } + public Membership id(Long id) { + this.id = id; + return this; + } + public void setId(Long id) { this.id = id; } @@ -176,6 +183,7 @@ public class Membership implements Serializable { public void setCustomer(Customer customer) { this.customer = customer; } + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here, do not remove @Override diff --git a/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java b/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java index d7153deb..9bff0e66 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java +++ b/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java @@ -6,7 +6,6 @@ import org.hostsharing.hsadminng.service.dto.CustomerDTO; import org.hostsharing.hsadminng.service.mapper.CustomerMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -19,7 +18,7 @@ import java.util.Optional; */ @Service @Transactional -public class CustomerService { +public class CustomerService implements DtoLoader { private final Logger log = LoggerFactory.getLogger(CustomerService.class); diff --git a/src/main/java/org/hostsharing/hsadminng/service/DtoLoader.java b/src/main/java/org/hostsharing/hsadminng/service/DtoLoader.java new file mode 100644 index 00000000..ae30d79b --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/DtoLoader.java @@ -0,0 +1,7 @@ +package org.hostsharing.hsadminng.service; + +import java.util.Optional; + +public interface DtoLoader { + Optional findOne(Long id); +} diff --git a/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java b/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java index 12e4fb40..d611e713 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java +++ b/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java @@ -19,7 +19,7 @@ import java.util.Optional; */ @Service @Transactional -public class MembershipService { +public class MembershipService implements DtoLoader { private final Logger log = LoggerFactory.getLogger(MembershipService.class); @@ -73,6 +73,7 @@ public class MembershipService { * @param id the id of the entity * @return the entity */ + @Override @Transactional(readOnly = true) public Optional findOne(Long id) { log.debug("Request to get Membership : {}", id); 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 c6faaf04..86d88bba 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java @@ -1,35 +1,33 @@ package org.hostsharing.hsadminng.service.accessfilter; +import org.apache.commons.lang3.NotImplementedException; import org.hostsharing.hsadminng.security.SecurityUtils; +import org.hostsharing.hsadminng.service.CustomerService; +import org.hostsharing.hsadminng.service.DtoLoader; +import org.hostsharing.hsadminng.service.MembershipService; +import org.hostsharing.hsadminng.service.dto.CustomerDTO; +import org.hostsharing.hsadminng.service.dto.MembershipDTO; import org.hostsharing.hsadminng.service.util.ReflectionUtil; +import org.springframework.context.ApplicationContext; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; abstract class JSonAccessFilter { + private final ApplicationContext ctx; final T dto; - Field selfIdField = null; - Field parentIdField = null; + final Field selfIdField; + final Field parentIdField; - JSonAccessFilter(final T dto) { + JSonAccessFilter(final ApplicationContext ctx, final T dto) { + this.ctx = ctx; this.dto = dto; - determineIdFields(); + this.selfIdField = determineFieldWithAnnotation(dto.getClass(), SelfId.class); + this.parentIdField = determineFieldWithAnnotation(dto.getClass(), ParentId.class); } - void determineIdFields() { - for (Field field : dto.getClass().getDeclaredFields()) { - if (field.isAnnotationPresent(SelfId.class)) { - if (selfIdField != null) { - throw new AssertionError("multiple @" + SelfId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName()); - } - selfIdField = field; - } - if (field.isAnnotationPresent(ParentId.class)) { - if (parentIdField != null) { - throw new AssertionError("multiple @" + ParentId.class.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName()); - } - parentIdField = field; - } - } + boolean isParentIdField(final Field field) { + return field.equals(parentIdField); } Long getId() { @@ -39,27 +37,68 @@ abstract class JSonAccessFilter { return (Long) ReflectionUtil.getValue(dto, selfIdField); } + /** + * @param field to get a display representation for + * @return a simplified, decently user readable, display representation of the given field + */ String toDisplay(final Field field) { return field.getDeclaringClass().getSimpleName() + "." + field.getName(); } + /** + * @return the role of the login user in relation to the dto, this filter is created for. + */ Role getLoginUserRole() { final Role roleOnSelf = getLoginUserRoleOnSelf(); - final Role roleOnParent = getLoginUserRoleOnParent(); - return roleOnSelf.covers(roleOnParent) ? roleOnSelf : roleOnParent; + if ( roleOnSelf.isIndependent() ) { + return roleOnSelf; + } + return getLoginUserRoleOnAncestorOfDtoClassIfHigher(roleOnSelf, dto); } - private Role getLoginUserRoleOnSelf() { - // TODO: find broadest role in self and recursively in parent return SecurityUtils.getLoginUserRoleFor(dto.getClass(), getId() ); } - private Role getLoginUserRoleOnParent() { + private Role getLoginUserRoleOnAncestorOfDtoClassIfHigher(final Role baseRole, final Object dto) { + final Field parentIdField = determineFieldWithAnnotation(dto.getClass(), ParentId.class); + if ( parentIdField == null ) { - return Role.ANYBODY; + return baseRole; } - final ParentId parentId = parentIdField.getAnnotation(ParentId.class); - return SecurityUtils.getLoginUserRoleFor(parentId.value(), (Long) ReflectionUtil.getValue(dto, parentIdField) ); + + final ParentId parentIdAnnot = parentIdField.getAnnotation(ParentId.class); + final Class parentDtoClass = parentIdAnnot.value(); + final Long parentId = (Long) ReflectionUtil.getValue(dto, parentIdField); + final Role roleOnParent = SecurityUtils.getLoginUserRoleFor(parentDtoClass, parentId); + + final Object parentEntity = findParentDto(parentDtoClass, parentId); + return Role.broadest(baseRole, getLoginUserRoleOnAncestorOfDtoClassIfHigher(roleOnParent, parentEntity)); + } + + private Object findParentDto(Class parentDtoClass, Long parentId) { + // TODO: generalize, e.g. via "all beans that implement DtoLoader + if ( parentDtoClass == MembershipDTO.class ) { + final DtoLoader dtoLoader = ctx.getAutowireCapableBeanFactory().createBean(MembershipService.class); + return dtoLoader.findOne(parentId).get(); + } + if ( parentDtoClass == CustomerDTO.class ) { + final DtoLoader dtoLoader = ctx.getAutowireCapableBeanFactory().createBean(CustomerService.class); + return dtoLoader.findOne(parentId).get(); + } + throw new NotImplementedException("no DtoLoader implemented for " + parentDtoClass); + } + + private static Field determineFieldWithAnnotation(final Class dtoClass, final Class idAnnotationClass) { + Field parentIdField = null; + for (Field field : dtoClass.getDeclaredFields()) { + if (field.isAnnotationPresent(idAnnotationClass)) { + if (parentIdField != null) { + throw new AssertionError("multiple @" + idAnnotationClass.getSimpleName() + " detected in " + field.getDeclaringClass().getSimpleName()); + } + parentIdField = field; + } + } + return parentIdField; } } 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 9dc104d1..5e918e84 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import org.apache.commons.lang3.NotImplementedException; import org.hostsharing.hsadminng.service.util.ReflectionUtil; import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; +import org.springframework.context.ApplicationContext; import java.lang.reflect.Field; import java.util.HashSet; @@ -21,8 +22,8 @@ public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { private final TreeNode treeNode; private final Set modifiedFields = new HashSet<>(); - public JSonDeserializerWithAccessFilter(final JsonParser jsonParser, final DeserializationContext deserializationContext, Class dtoClass) { - super(unchecked(dtoClass::newInstance)); + public JSonDeserializerWithAccessFilter(final ApplicationContext ctx, final JsonParser jsonParser, final DeserializationContext deserializationContext, Class dtoClass) { + super(ctx, unchecked(dtoClass::newInstance)); this.treeNode = unchecked(() -> jsonParser.getCodec().readTree(jsonParser)); } 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 b2ec0c57..20d38832 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -4,18 +4,21 @@ package org.hostsharing.hsadminng.service.accessfilter; 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.time.LocalDate; public class JSonSerializerWithAccessFilter extends JSonAccessFilter { private final JsonGenerator jsonGenerator; private final SerializerProvider serializerProvider; - public JSonSerializerWithAccessFilter(final JsonGenerator jsonGenerator, + public JSonSerializerWithAccessFilter(final ApplicationContext ctx, + final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider, final T dto) { - super(dto); + super(ctx, dto); this.jsonGenerator = jsonGenerator; this.serializerProvider = serializerProvider; } @@ -42,6 +45,10 @@ public class JSonSerializerWithAccessFilter extends JSonAccessFilter { jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { jsonGenerator.writeNumberField(fieldName, (long) get(dto, prop)); + } else if (LocalDate.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeStringField(fieldName, get(dto, prop).toString()); // TODO proper format + } else if (Enum.class.isAssignableFrom(prop.getType())) { + jsonGenerator.writeStringField(fieldName, get(dto, prop).toString()); // TODO proper representation } else if (String.class.isAssignableFrom(prop.getType())) { jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); } else { diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java index 83f667d0..8fbe8c44 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java @@ -87,6 +87,26 @@ public enum Role { this.level = level; } + /** + * @return true if this role is independent of a target object, false otherwise. + */ + public boolean isIndependent() { + return covers(Role.SUPPORTER); + } + + /** + @return the role with the broadest access rights + */ + public static Role broadest(final Role role, final Role... roles) { + Role broadests = role; + for ( Role r: roles ) { + if ( r.covers(broadests)) { + broadests = r; + } + } + return broadests; + } + /** * Determines if the given role is covered by this role. * @@ -163,5 +183,4 @@ public enum Role { } return false; } - } diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index 28a84d2a..bae6ca25 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.hostsharing.hsadminng.service.accessfilter.*; import org.springframework.boot.jackson.JsonComponent; +import org.springframework.context.ApplicationContext; import javax.validation.constraints.*; import java.io.IOException; @@ -172,24 +173,34 @@ public class CustomerDTO implements Serializable { @JsonComponent public static class CustomerJsonSerializer extends JsonSerializer { + private final ApplicationContext ctx; + + public CustomerJsonSerializer(final ApplicationContext ctx) { + this.ctx = ctx; + } + @Override public void serialize(final CustomerDTO customerDTO, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException { - new JSonSerializerWithAccessFilter<>(jsonGenerator, serializerProvider, customerDTO).serialize(); + new JSonSerializerWithAccessFilter<>(ctx, jsonGenerator, serializerProvider, customerDTO).serialize(); } } - - @JsonComponent public static class CustomerJsonDeserializer extends JsonDeserializer { + private final ApplicationContext ctx; + + public CustomerJsonDeserializer(final ApplicationContext ctx) { + this.ctx = ctx; + } + @Override public CustomerDTO deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) { - return new JSonDeserializerWithAccessFilter<>(jsonParser, deserializationContext, CustomerDTO.class).deserialize(); + return new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, deserializationContext, CustomerDTO.class).deserialize(); } } } 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 15130c3c..c255c3a2 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java @@ -37,10 +37,10 @@ public class MembershipDTO implements Serializable { private String remark; @ParentId(CustomerDTO.class) - @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long customerId; - @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String customerPrefix; public MembershipDTO with( diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java index e9f873fa..eff5bad4 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java @@ -1,35 +1,51 @@ package org.hostsharing.hsadminng.service.dto; -import java.time.LocalDate; -import javax.validation.constraints.*; -import java.io.Serializable; -import java.util.Objects; + import org.hostsharing.hsadminng.domain.enumeration.ShareAction; +import org.hostsharing.hsadminng.service.accessfilter.AccessFor; +import org.hostsharing.hsadminng.service.accessfilter.ParentId; +import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.accessfilter.SelfId; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; /** * A DTO for the Share entity. */ public class ShareDTO implements Serializable { + @SelfId + @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long id; @NotNull + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate documentDate; @NotNull + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate valueDate; @NotNull + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private ShareAction action; @NotNull + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Integer quantity; @Size(max = 160) + @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) private String remark; - + @ParentId(MembershipDTO.class) + @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long membershipId; + @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) private String membershipDocumentDate; public Long getId() { diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java new file mode 100644 index 00000000..ec1d6487 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java @@ -0,0 +1,27 @@ +package org.hostsharing.hsadminng.service.accessfilter; + +import org.apache.commons.lang3.tuple.ImmutablePair; + +public class JSonBuilder { + + @SafeVarargs + public static String asJSon(final ImmutablePair... properties) { + final StringBuilder json = new StringBuilder(); + for (ImmutablePair prop : properties) { + json.append(inQuotes(prop.left)); + json.append(": "); + if (prop.right instanceof Number) { + json.append(prop.right); + } else { + json.append(inQuotes(prop.right)); + } + json.append(",\n"); + } + return "{\n" + json.substring(0, json.length() - 2) + "\n}"; + } + + private static String inQuotes(Object value) { + return "\"" + value.toString() + "\""; + } + +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index ed3fa7fb..9c0340b8 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -12,11 +12,13 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.springframework.context.ApplicationContext; import java.io.IOException; 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.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; import static org.mockito.BDDMockito.given; @@ -27,6 +29,9 @@ public class JSonDeserializerWithAccessFilterUnitTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock + public ApplicationContext ctx; + @Mock public JsonParser jsonParser; @@ -52,7 +57,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { ImmutablePair.of("openStringField", "String Value"))); // when - GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize(); // then assertThat(actualDto.openStringField).isEqualTo("String Value"); @@ -66,7 +71,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { ImmutablePair.of("openIntegerField", 1234))); // when - GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize(); // then assertThat(actualDto.openIntegerField).isEqualTo(1234); @@ -80,7 +85,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { ImmutablePair.of("openLongField", 1234L))); // when - GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize(); // then assertThat(actualDto.openLongField).isEqualTo(1234L); @@ -96,7 +101,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { ImmutablePair.of("restrictedField", "Restricted String Value"))); // when - GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize(); + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize(); // then assertThat(actualDto.restrictedField).isEqualTo("Restricted String Value"); @@ -110,7 +115,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); // when - Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize()); + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize()); // then assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { @@ -127,7 +132,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { givenJSonTree(asJSon(ImmutablePair.of("parentId", 1111L))); // when - Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenChildDto.class).deserialize()); + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenChildDto.class).deserialize()); // then assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { @@ -144,7 +149,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { givenJSonTree(asJSon(ImmutablePair.of("parentId", 1111L))); // when - final GivenChildDto actualDto = new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenChildDto.class).deserialize(); + final GivenChildDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenChildDto.class).deserialize(); // then assertThat(actualDto.parentId).isEqualTo(1111L); @@ -160,7 +165,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { ImmutablePair.of("restrictedField", "Restricted String Value"))); // when - Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDto.class).deserialize()); + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize()); // then assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { @@ -175,7 +180,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { givenJSonTree(asJSon(ImmutablePair.of("id", 1111L))); // when - Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(jsonParser, null, GivenDtoWithMultipleSelfId.class).deserialize()); + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDtoWithMultipleSelfId.class).deserialize()); // then assertThat(exception).isInstanceOf(AssertionError.class).hasMessage("multiple @SelfId detected in GivenDtoWithMultipleSelfId"); @@ -183,30 +188,10 @@ public class JSonDeserializerWithAccessFilterUnitTest { // --- only fixture code below --- - @SafeVarargs - private final String asJSon(final ImmutablePair... properties) { - final StringBuilder json = new StringBuilder(); - for (ImmutablePair prop : properties) { - json.append(inQuotes(prop.left)); - json.append(": "); - if (prop.right instanceof Number) { - json.append(prop.right); - } else { - json.append(inQuotes(prop.right)); - } - json.append(",\n"); - } - return "{\n" + json.substring(0, json.length() - 2) + "\n}"; - } - private void givenJSonTree(String givenJSon) throws IOException { given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); } - private String inQuotes(Object value) { - return "\"" + value.toString() + "\""; - } - public static class GivenDto { @SelfId diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java index 7d5265fc..8cd09db8 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.springframework.context.ApplicationContext; import java.io.IOException; @@ -23,6 +24,9 @@ public class JSonSerializerWithAccessFilterUnitTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock + public ApplicationContext ctx; + @Mock public JsonGenerator jsonGenerator; @@ -37,7 +41,7 @@ public class JSonSerializerWithAccessFilterUnitTest { @Test public void shouldSerializeStringField() throws IOException { // when - new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); + new JSonSerializerWithAccessFilter<>(ctx, jsonGenerator, null, givenDTO).serialize(); // then verify(jsonGenerator).writeStringField("openStringField", givenDTO.openStringField); @@ -51,7 +55,7 @@ public class JSonSerializerWithAccessFilterUnitTest { MockSecurityContext.givenUserHavingRole(GivenCustomerDto.class, 888L, Role.FINANCIAL_CONTACT); // when - new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); + new JSonSerializerWithAccessFilter<>(ctx, jsonGenerator, null, givenDTO).serialize(); // then verify(jsonGenerator).writeStringField("restrictedField", givenDTO.restrictedField); @@ -65,7 +69,7 @@ public class JSonSerializerWithAccessFilterUnitTest { MockSecurityContext.givenUserHavingRole(GivenCustomerDto.class, 888L, Role.ANY_CUSTOMER_USER); // when - new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDTO).serialize(); + new JSonSerializerWithAccessFilter<>(ctx, jsonGenerator, null, givenDTO).serialize(); // then verify(jsonGenerator, never()).writeStringField("restrictedField", givenDTO.restrictedField); @@ -84,7 +88,7 @@ public class JSonSerializerWithAccessFilterUnitTest { final GivenDtoWithUnimplementedFieldType givenDtoWithUnimplementedFieldType = new GivenDtoWithUnimplementedFieldType(); // when - Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter<>(jsonGenerator, null, givenDtoWithUnimplementedFieldType).serialize()); + final Throwable actual = catchThrowable(() -> new JSonSerializerWithAccessFilter<>(ctx, jsonGenerator, null, givenDtoWithUnimplementedFieldType).serialize()); // then assertThat(actual).isInstanceOf(NotImplementedException.class); diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java index 5b3f78b2..cfa66edf 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java @@ -19,6 +19,9 @@ public class MockSecurityContext { } public static void givenUserHavingRole(final Class onClass, final Long onId, final Role role) { + if ((onClass == null || onId == null) && !role.isIndependent()) { + throw new IllegalArgumentException("dependent roles like " + role + " depend on DtoClass and ID"); + } SecurityUtils.addUserRole(onClass, onId, role); } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java new file mode 100644 index 00000000..a4976186 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java @@ -0,0 +1,85 @@ +package org.hostsharing.hsadminng.service.dto; + +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.hostsharing.hsadminng.service.accessfilter.JSonDeserializerWithAccessFilter; +import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.springframework.context.ApplicationContext; + +import java.io.IOException; + +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.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; +import static org.mockito.BDDMockito.given; + +public class MembershipDTOUnitTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + public ApplicationContext ctx; + + @Mock + public JsonParser jsonParser; + + @Mock + public ObjectCodec codec; + + @Mock + public TreeNode treeNode; + + @Before + public void init() { + given(jsonParser.getCodec()).willReturn(codec); + } + + @Test + public void adminShouldHaveRightToCreate() throws IOException { + givenAuthenticatedUser(); + givenUserHavingRole(null, null, Role.ADMIN); + givenJSonTree(asJSon(ImmutablePair.of("customerId", 1234L))); + + // when + final MembershipDTO actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, MembershipDTO.class).deserialize(); + + // then + assertThat(actualDto.getCustomerId()).isEqualTo(1234L); + } + + @Test + public void contractualContactShouldNotHaveRightToCreate() throws IOException { + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, 1234L, Role.CONTRACTUAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("customerId", 1234L))); + + // when + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, MembershipDTO.class).deserialize()); + + // then + assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { + assertThat(badRequestAlertException.getParam()).isEqualTo("MembershipDTO.customerId"); + assertThat(badRequestAlertException.getErrorKey()).isEqualTo("referencingProhibited"); + }); + } + + // --- only fixture code below --- + + private void givenJSonTree(String givenJSon) throws IOException { + given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); + } + +} diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java new file mode 100644 index 00000000..b42e6ac6 --- /dev/null +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java @@ -0,0 +1,163 @@ +package org.hostsharing.hsadminng.service.dto; + +import com.fasterxml.jackson.core.JsonGenerator; +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.hostsharing.hsadminng.domain.enumeration.ShareAction; +import org.hostsharing.hsadminng.service.CustomerService; +import org.hostsharing.hsadminng.service.MembershipService; +import org.hostsharing.hsadminng.service.accessfilter.JSonDeserializerWithAccessFilter; +import org.hostsharing.hsadminng.service.accessfilter.JSonSerializerWithAccessFilter; +import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; +import org.junit.Before; +import org.junit.Rule; +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.time.LocalDate; +import java.util.Optional; + +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.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class ShareDTOUnitTest { + + private static final long SOME_MEMBERSHIP_ID = 12345L; + private static final long SOME_CUSTOMER_ID = 1234L; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private ApplicationContext ctx; + + @Mock + private AutowireCapableBeanFactory autowireCapableBeanFactory; + + @Mock + private JsonParser jsonParser; + + @Mock + private JsonGenerator jsonGenerator; + + @Mock + private ObjectCodec codec; + + @Mock + private TreeNode treeNode; + + @Mock + private CustomerService customerService; + + @Mock + private MembershipService membershipService; + + @Before + public void init() { + given(jsonParser.getCodec()).willReturn(codec); + + given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); + given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); + given(autowireCapableBeanFactory.createBean(CustomerService.class)).willReturn(customerService); + given(autowireCapableBeanFactory.createBean(MembershipService.class)).willReturn(membershipService); + + given(customerService.findOne(SOME_CUSTOMER_ID)).willReturn(Optional.of(new CustomerDTO())); + given(membershipService.findOne(SOME_MEMBERSHIP_ID)).willReturn(Optional.of(new MembershipDTO().with(dto -> dto.setCustomerId(SOME_CUSTOMER_ID)))); + } + + @Test + public void adminShouldHaveRightToCreate() throws IOException { + givenAuthenticatedUser(); + givenUserHavingRole(null, null, Role.ADMIN); + givenJSonTree(asJSon(ImmutablePair.of("membershipId", SOME_MEMBERSHIP_ID))); + + // when + final ShareDTO actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, ShareDTO.class).deserialize(); + + // then + assertThat(actualDto.getMembershipId()).isEqualTo(SOME_MEMBERSHIP_ID); + } + + @Test + public void contractualContactShouldNotHaveRightToCreate() throws IOException { + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.CONTRACTUAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("membershipId", ShareDTOUnitTest.SOME_MEMBERSHIP_ID))); + + // when + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, ShareDTO.class).deserialize()); + + // then + assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { + assertThat(badRequestAlertException.getParam()).isEqualTo("ShareDTO.membershipId"); + assertThat(badRequestAlertException.getErrorKey()).isEqualTo("referencingProhibited"); + }); + } + + @Test + public void financialContactShouldHaveRightToReadAllButRemark() throws IOException { + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, SOME_CUSTOMER_ID, Role.FINANCIAL_CONTACT); + final ShareDTO givenDTO = createShareDto(); + + // when + new JSonSerializerWithAccessFilter<>(ctx, jsonGenerator, null, givenDTO).serialize(); + + // then + verify(jsonGenerator).writeNumberField("id", givenDTO.getId()); + verify(jsonGenerator).writeNumberField("membershipId", givenDTO.getMembershipId()); + verify(jsonGenerator, never()).writeStringField(eq("remark"), anyString()); + } + + @Test + public void supporterShouldHaveRightToRead() throws IOException { + givenAuthenticatedUser(); + givenUserHavingRole(null, null, Role.SUPPORTER); + final ShareDTO givenDTO = createShareDto(); + + // when + new JSonSerializerWithAccessFilter<>(ctx, jsonGenerator, null, givenDTO).serialize(); + + // then + verify(jsonGenerator).writeNumberField("id", givenDTO.getId()); + verify(jsonGenerator).writeNumberField("membershipId", givenDTO.getMembershipId()); + verify(jsonGenerator).writeStringField("remark", givenDTO.getRemark()); + } + + // --- only fixture code below --- + + private void givenJSonTree(String givenJSon) throws IOException { + given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); + } + + private ShareDTO createShareDto() { + final ShareDTO givenDTO = new ShareDTO(); + givenDTO.setId(1234567L); + givenDTO.setMembershipId(SOME_MEMBERSHIP_ID); + givenDTO.setAction(ShareAction.SUBSCRIPTION); + givenDTO.setQuantity(3); + givenDTO.setDocumentDate(LocalDate.parse("2019-04-22")); + givenDTO.setMembershipDocumentDate("2019-04-21"); // TODO: why is this not a LocalDate? + givenDTO.setValueDate(LocalDate.parse("2019-04-30")); + givenDTO.setRemark("Some Remark"); + return givenDTO; + } + +} From ad1517a16e56c31005af9868b59730d414cd90f7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 24 Apr 2019 13:23:08 +0200 Subject: [PATCH 22/24] JSonAccessFilter with generic access to grand parent role --- .../hsadminng/service/CustomerService.java | 2 +- .../{DtoLoader.java => IdToDtoResolver.java} | 2 +- .../hsadminng/service/MembershipService.java | 2 +- .../accessfilter/JSonAccessFilter.java | 47 +++++++++++-------- .../service/accessfilter/ParentId.java | 6 ++- .../hsadminng/service/dto/MembershipDTO.java | 3 +- .../hsadminng/service/dto/ShareDTO.java | 3 +- ...nDeserializerWithAccessFilterUnitTest.java | 6 ++- ...SonSerializerWithAccessFilterUnitTest.java | 6 ++- 9 files changed, 48 insertions(+), 29 deletions(-) rename src/main/java/org/hostsharing/hsadminng/service/{DtoLoader.java => IdToDtoResolver.java} (75%) diff --git a/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java b/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java index 9bff0e66..06d8d978 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java +++ b/src/main/java/org/hostsharing/hsadminng/service/CustomerService.java @@ -18,7 +18,7 @@ import java.util.Optional; */ @Service @Transactional -public class CustomerService implements DtoLoader { +public class CustomerService implements IdToDtoResolver { private final Logger log = LoggerFactory.getLogger(CustomerService.class); diff --git a/src/main/java/org/hostsharing/hsadminng/service/DtoLoader.java b/src/main/java/org/hostsharing/hsadminng/service/IdToDtoResolver.java similarity index 75% rename from src/main/java/org/hostsharing/hsadminng/service/DtoLoader.java rename to src/main/java/org/hostsharing/hsadminng/service/IdToDtoResolver.java index ae30d79b..14b15828 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/DtoLoader.java +++ b/src/main/java/org/hostsharing/hsadminng/service/IdToDtoResolver.java @@ -2,6 +2,6 @@ package org.hostsharing.hsadminng.service; import java.util.Optional; -public interface DtoLoader { +public interface IdToDtoResolver { Optional findOne(Long id); } diff --git a/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java b/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java index d611e713..473f5d7e 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java +++ b/src/main/java/org/hostsharing/hsadminng/service/MembershipService.java @@ -19,7 +19,7 @@ import java.util.Optional; */ @Service @Transactional -public class MembershipService implements DtoLoader { +public class MembershipService implements IdToDtoResolver { private final Logger log = LoggerFactory.getLogger(MembershipService.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 86d88bba..45d2b7ac 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java @@ -1,17 +1,16 @@ package org.hostsharing.hsadminng.service.accessfilter; -import org.apache.commons.lang3.NotImplementedException; import org.hostsharing.hsadminng.security.SecurityUtils; -import org.hostsharing.hsadminng.service.CustomerService; -import org.hostsharing.hsadminng.service.DtoLoader; -import org.hostsharing.hsadminng.service.MembershipService; -import org.hostsharing.hsadminng.service.dto.CustomerDTO; +import org.hostsharing.hsadminng.service.IdToDtoResolver; import org.hostsharing.hsadminng.service.dto.MembershipDTO; import org.hostsharing.hsadminng.service.util.ReflectionUtil; import org.springframework.context.ApplicationContext; +import javax.persistence.EntityNotFoundException; import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; abstract class JSonAccessFilter { private final ApplicationContext ctx; @@ -50,43 +49,51 @@ abstract class JSonAccessFilter { */ Role getLoginUserRole() { final Role roleOnSelf = getLoginUserRoleOnSelf(); - if ( roleOnSelf.isIndependent() ) { + if (roleOnSelf.isIndependent()) { return roleOnSelf; } return getLoginUserRoleOnAncestorOfDtoClassIfHigher(roleOnSelf, dto); } private Role getLoginUserRoleOnSelf() { - return SecurityUtils.getLoginUserRoleFor(dto.getClass(), getId() ); + return SecurityUtils.getLoginUserRoleFor(dto.getClass(), getId()); } private Role getLoginUserRoleOnAncestorOfDtoClassIfHigher(final Role baseRole, final Object dto) { final Field parentIdField = determineFieldWithAnnotation(dto.getClass(), ParentId.class); - if ( parentIdField == null ) { + if (parentIdField == null) { return baseRole; } final ParentId parentIdAnnot = parentIdField.getAnnotation(ParentId.class); - final Class parentDtoClass = parentIdAnnot.value(); + final Class parentDtoLoader = parentIdAnnot.resolver(); + final Class parentDtoClass = getGenericClassParameter(parentDtoLoader); final Long parentId = (Long) ReflectionUtil.getValue(dto, parentIdField); final Role roleOnParent = SecurityUtils.getLoginUserRoleFor(parentDtoClass, parentId); - final Object parentEntity = findParentDto(parentDtoClass, parentId); + final Object parentEntity = findParentDto(parentDtoLoader, parentId); return Role.broadest(baseRole, getLoginUserRoleOnAncestorOfDtoClassIfHigher(roleOnParent, parentEntity)); } - private Object findParentDto(Class parentDtoClass, Long parentId) { - // TODO: generalize, e.g. via "all beans that implement DtoLoader - if ( parentDtoClass == MembershipDTO.class ) { - final DtoLoader dtoLoader = ctx.getAutowireCapableBeanFactory().createBean(MembershipService.class); - return dtoLoader.findOne(parentId).get(); + @SuppressWarnings("unchecked") + private Class getGenericClassParameter(Class parentDtoLoader) { + for (Type genericInterface : parentDtoLoader.getGenericInterfaces()) { + if (genericInterface instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) genericInterface; + if (parameterizedType.getRawType()== IdToDtoResolver.class) { + return (Class) parameterizedType.getActualTypeArguments()[0]; + } + } + } - if ( parentDtoClass == CustomerDTO.class ) { - final DtoLoader dtoLoader = ctx.getAutowireCapableBeanFactory().createBean(CustomerService.class); - return dtoLoader.findOne(parentId).get(); - } - throw new NotImplementedException("no DtoLoader implemented for " + parentDtoClass); + throw new AssertionError(parentDtoLoader.getSimpleName() + " expected to implement " + IdToDtoResolver.class.getSimpleName() + "<...DTO>"); + } + + @SuppressWarnings("unchecked") + private Object findParentDto(final Class parentDtoLoader, final Long parentId) { + final IdToDtoResolver idToDtoResolver = ctx.getAutowireCapableBeanFactory().createBean(parentDtoLoader); + return idToDtoResolver.findOne(parentId).orElseThrow(() -> new EntityNotFoundException("Can't resolve parent entity ID " + parentId + " via " + parentDtoLoader)); } private static Field determineFieldWithAnnotation(final Class dtoClass, final Class idAnnotationClass) { diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java index 51ddc0a2..e392374e 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/ParentId.java @@ -1,5 +1,7 @@ package org.hostsharing.hsadminng.service.accessfilter; +import org.hostsharing.hsadminng.service.IdToDtoResolver; + import java.lang.annotation.*; /** @@ -12,6 +14,6 @@ import java.lang.annotation.*; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface ParentId { - /// The DTO class of the referenced entity. - Class value(); + /// The service which can load the referenced DTO. + Class> resolver(); } 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 c255c3a2..141614d9 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java @@ -1,5 +1,6 @@ package org.hostsharing.hsadminng.service.dto; +import org.hostsharing.hsadminng.service.CustomerService; import org.hostsharing.hsadminng.service.accessfilter.AccessFor; import org.hostsharing.hsadminng.service.accessfilter.ParentId; import org.hostsharing.hsadminng.service.accessfilter.Role; @@ -36,7 +37,7 @@ public class MembershipDTO implements Serializable { @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) private String remark; - @ParentId(CustomerDTO.class) + @ParentId(resolver = CustomerService.class) @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long customerId; diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java index eff5bad4..b9e79209 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java @@ -1,6 +1,7 @@ package org.hostsharing.hsadminng.service.dto; import org.hostsharing.hsadminng.domain.enumeration.ShareAction; +import org.hostsharing.hsadminng.service.MembershipService; import org.hostsharing.hsadminng.service.accessfilter.AccessFor; import org.hostsharing.hsadminng.service.accessfilter.ParentId; import org.hostsharing.hsadminng.service.accessfilter.Role; @@ -41,7 +42,7 @@ public class ShareDTO implements Serializable { @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) private String remark; - @ParentId(MembershipDTO.class) + @ParentId(resolver = MembershipService.class) @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long membershipId; diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index 9c0340b8..da59b241 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -5,6 +5,7 @@ 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.hostsharing.hsadminng.service.IdToDtoResolver; import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; import org.junit.Before; import org.junit.Rule; @@ -211,6 +212,9 @@ public class JSonDeserializerWithAccessFilterUnitTest { Long openLongField; } + abstract class GivenService implements IdToDtoResolver { + } + public static class GivenChildDto { @SelfId @@ -218,7 +222,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { Long id; @AccessFor(init = Role.CONTRACTUAL_CONTACT, update = Role.CONTRACTUAL_CONTACT, read = Role.ACTUAL_CUSTOMER_USER) - @ParentId(GivenDto.class) + @ParentId(resolver = GivenService.class) Long parentId; @AccessFor(init = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}, update = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java index 8cd09db8..f953bcf1 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; +import org.hostsharing.hsadminng.service.IdToDtoResolver; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -110,9 +111,12 @@ public class JSonSerializerWithAccessFilterUnitTest { } + private abstract class GivenCustomerService implements IdToDtoResolver { + } + private static class GivenDto { - @ParentId(GivenCustomerDto.class) + @ParentId(resolver = GivenCustomerService.class) Long customerId; @AccessFor(read = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) From 3e30cf2d1771a8b2c6e10c85fae67aa08d317444 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 25 Apr 2019 12:22:45 +0200 Subject: [PATCH 23/24] merging current DTO during deserialization --- .../hsadminng/security/SecurityUtils.java | 3 +- .../hsadminng/service/IdToDtoResolver.java | 2 +- .../hsadminng/service/ShareService.java | 2 +- .../accessfilter/JSonAccessFilter.java | 20 +++- .../JSonDeserializerWithAccessFilter.java | 78 +++++++++++---- .../JSonSerializerWithAccessFilter.java | 37 +++---- .../hsadminng/service/accessfilter/Role.java | 12 +-- .../service/accessfilter/SelfId.java | 4 + .../hsadminng/service/dto/AssetDTO.java | 12 +-- .../hsadminng/service/dto/CustomerDTO.java | 18 +++- .../hsadminng/service/dto/FluentBuilder.java | 15 +++ .../hsadminng/service/dto/MembershipDTO.java | 12 +-- .../hsadminng/service/dto/ShareDTO.java | 26 +++-- .../hsadminng/service/mapper/AssetMapper.java | 2 +- .../hsadminng/service/mapper/ShareMapper.java | 2 +- .../hsadminng/web/rest/CustomerResource.java | 13 ++- .../asset/asset-detail.component.html | 2 +- .../app/entities/asset/asset.component.html | 4 +- .../share/share-detail.component.html | 2 +- .../app/entities/share/share.component.html | 4 +- .../webapp/app/shared/model/asset.model.ts | 4 +- .../webapp/app/shared/model/share.model.ts | 4 +- src/main/webapp/i18n/de/custom-error.json | 1 + src/main/webapp/i18n/en/custom-error.json | 1 + ...nDeserializerWithAccessFilterUnitTest.java | 96 +++++++++++++++---- ...SonSerializerWithAccessFilterUnitTest.java | 38 +++++++- .../accessfilter/MockSecurityContext.java | 7 +- .../service/accessfilter/RoleUnitTest.java | 4 +- .../service/dto/CustomerDTOUnitTest.java | 95 +++++++++++++++--- .../service/dto/MembershipDTOUnitTest.java | 28 +++++- .../service/dto/ShareDTOUnitTest.java | 2 +- .../web/rest/CustomerResourceIntTest.java | 44 +++++---- 32 files changed, 431 insertions(+), 163 deletions(-) create mode 100644 src/main/java/org/hostsharing/hsadminng/service/dto/FluentBuilder.java diff --git a/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java b/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java index e06fe195..f5a4a1cb 100644 --- a/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java +++ b/src/main/java/org/hostsharing/hsadminng/security/SecurityUtils.java @@ -80,7 +80,6 @@ public final class SecurityUtils { } public static Role getLoginUserRoleFor(final Class onDtoClass, final Long onId) { - final Role highestRole = userRoleAssignments.stream(). map(ura -> matches(onDtoClass, onId, ura) @@ -91,7 +90,7 @@ public final class SecurityUtils { } private static boolean matches(Class onDtoClass, Long onId, UserRoleAssignment ura) { - final boolean matches = (ura.onClass == null || onDtoClass == ura.onClass) && (ura.onId == null || onId.equals(ura.onId) ); + final boolean matches = (ura.onClass == null || onDtoClass == ura.onClass) && (ura.onId == null || ura.onId.equals(onId)); return matches; } diff --git a/src/main/java/org/hostsharing/hsadminng/service/IdToDtoResolver.java b/src/main/java/org/hostsharing/hsadminng/service/IdToDtoResolver.java index 14b15828..32851fb6 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/IdToDtoResolver.java +++ b/src/main/java/org/hostsharing/hsadminng/service/IdToDtoResolver.java @@ -3,5 +3,5 @@ package org.hostsharing.hsadminng.service; import java.util.Optional; public interface IdToDtoResolver { - Optional findOne(Long id); + Optional findOne(Long id); } diff --git a/src/main/java/org/hostsharing/hsadminng/service/ShareService.java b/src/main/java/org/hostsharing/hsadminng/service/ShareService.java index f144bf9b..c9ff868b 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/ShareService.java +++ b/src/main/java/org/hostsharing/hsadminng/service/ShareService.java @@ -19,7 +19,7 @@ import java.util.Optional; */ @Service @Transactional -public class ShareService { +public class ShareService implements IdToDtoResolver { private final Logger log = LoggerFactory.getLogger(ShareService.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 45d2b7ac..e58e2046 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonAccessFilter.java @@ -4,14 +4,17 @@ import org.hostsharing.hsadminng.security.SecurityUtils; import org.hostsharing.hsadminng.service.IdToDtoResolver; import org.hostsharing.hsadminng.service.dto.MembershipDTO; import org.hostsharing.hsadminng.service.util.ReflectionUtil; +import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; -import javax.persistence.EntityNotFoundException; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import static com.google.common.base.Verify.verify; + abstract class JSonAccessFilter { private final ApplicationContext ctx; final T dto; @@ -72,7 +75,7 @@ abstract class JSonAccessFilter { final Long parentId = (Long) ReflectionUtil.getValue(dto, parentIdField); final Role roleOnParent = SecurityUtils.getLoginUserRoleFor(parentDtoClass, parentId); - final Object parentEntity = findParentDto(parentDtoLoader, parentId); + final Object parentEntity = loadDto(parentDtoLoader, parentId); return Role.broadest(baseRole, getLoginUserRoleOnAncestorOfDtoClassIfHigher(roleOnParent, parentEntity)); } @@ -91,9 +94,16 @@ abstract class JSonAccessFilter { } @SuppressWarnings("unchecked") - private Object findParentDto(final Class parentDtoLoader, final Long parentId) { - final IdToDtoResolver idToDtoResolver = ctx.getAutowireCapableBeanFactory().createBean(parentDtoLoader); - return idToDtoResolver.findOne(parentId).orElseThrow(() -> new EntityNotFoundException("Can't resolve parent entity ID " + parentId + " via " + parentDtoLoader)); + protected Object loadDto(final Class resolverClass, final Long id) { + verify(id != null, "id must not be null"); + + final AutowireCapableBeanFactory beanFactory = ctx.getAutowireCapableBeanFactory(); + verify(beanFactory != null, "no bean factory found, probably missing mock configuration for ApplicationContext, e.g. given(...)"); + + final IdToDtoResolver resolverBean = beanFactory.createBean(resolverClass); + verify(resolverBean != null, "no " + resolverClass.getSimpleName() + " bean created, probably missing mock configuration for AutowireCapableBeanFactory, e.g. given(...)"); + + return resolverBean.findOne(id).orElseThrow(() -> new BadRequestAlertException("Can't resolve entity ID " + id + " via " + resolverClass, resolverClass.getSimpleName(), "isNotFound")); } private static Field determineFieldWithAnnotation(final Class dtoClass, final Class idAnnotationClass) { 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 5e918e84..de4085ec 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -3,15 +3,18 @@ package org.hostsharing.hsadminng.service.accessfilter; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.TextNode; import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.ObjectUtils; import org.hostsharing.hsadminng.service.util.ReflectionUtil; import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; import org.springframework.context.ApplicationContext; import java.lang.reflect.Field; +import java.time.LocalDate; import java.util.HashSet; import java.util.Set; @@ -20,7 +23,7 @@ import static org.hostsharing.hsadminng.service.util.ReflectionUtil.unchecked; public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { private final TreeNode treeNode; - private final Set modifiedFields = new HashSet<>(); + private final Set writtenFields = new HashSet<>(); public JSonDeserializerWithAccessFilter(final ApplicationContext ctx, final JsonParser jsonParser, final DeserializationContext deserializationContext, Class dtoClass) { super(ctx, unchecked(dtoClass::newInstance)); @@ -30,7 +33,9 @@ public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { // Jackson deserializes from the JsonParser, thus no input parameter needed. public T deserialize() { deserializeValues(); - checkAccessToModifiedFields(); + final T currentDto = loadCurrentDto(getId()); + overwriteUnmodifiedFieldsWithCurrentValues(currentDto); + checkAccessToWrittenFields(currentDto); return dto; } @@ -38,25 +43,52 @@ public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { treeNode.fieldNames().forEachRemaining(fieldName -> { try { final Field field = dto.getClass().getDeclaredField(fieldName); - final Object value = readValue(treeNode, field); - writeValue(dto, field, value); - markAsModified(field); + final Object newValue = readValue(treeNode, field); + writeValue(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 Object currentDto) { + if ( currentDto == null ) { + return; + } + for (Field field : currentDto.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(AccessFor.class) && !writtenFields.contains(field)) { + final Object value = ReflectionUtil.getValue(currentDto, field); + ReflectionUtil.setValue(dto, field, value); + } + + } + } + private Object readValue(final TreeNode treeNode, final Field field) { - final TreeNode fieldNode = treeNode.get(field.getName()); + return readValue(treeNode, field.getName(), field.getType()); + + } + + private Object readValue(final TreeNode treeNode, final String fieldName, final Class fieldClass) { + final TreeNode fieldNode = treeNode.get(fieldName); if (fieldNode instanceof TextNode) { return ((TextNode) fieldNode).asText(); } else if (fieldNode instanceof IntNode) { return ((IntNode) fieldNode).asInt(); } else if (fieldNode instanceof LongNode) { return ((LongNode) fieldNode).asLong(); + } else 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()); } else { - throw new NotImplementedException("property type not yet implemented: " + field); + throw new NotImplementedException("property type not yet implemented: " + fieldNode + " -> " + fieldName + ": " + fieldClass); } } @@ -67,30 +99,36 @@ public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { 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 (field.getType().isEnum()) { + ReflectionUtil.setValue(dto, field, Enum.valueOf((Class) field.getType(), value.toString())); + } 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); } + writtenFields.add(field); } - private void markAsModified(final Field field) { - modifiedFields.add(field); - } - - private void checkAccessToModifiedFields() { - modifiedFields.forEach(field -> { - if ( !field.equals(selfIdField) ) { + private void checkAccessToWrittenFields(final T currentDto) { + writtenFields.forEach(field -> { + if (!field.equals(selfIdField)) { + final Role role = getLoginUserRole(); if (getId() == null) { - if (!getLoginUserRole().isAllowedToInit(field)) { - if ( !field.equals(parentIdField)) { - throw new BadRequestAlertException("Initialization of field prohibited for current user", toDisplay(field), "initializationProhibited"); + if (!role.isAllowedToInit(field)) { + if (!field.equals(parentIdField)) { + throw new BadRequestAlertException("Initialization of field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "initializationProhibited"); } else { - throw new BadRequestAlertException("Referencing field prohibited for current user", toDisplay(field), "referencingProhibited"); + throw new BadRequestAlertException("Referencing field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "referencingProhibited"); } } - } else if (!getLoginUserRole().isAllowedToUpdate(field)) { - throw new BadRequestAlertException("Update of field prohibited for current user", toDisplay(field), "updateProhibited"); + } else if (isUpdate(field, dto, currentDto) && !getLoginUserRole().isAllowedToUpdate(field)){ + throw new BadRequestAlertException("Update of field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "updateProhibited"); } } }); } + + private boolean isUpdate(final Field field, final T dto, T currentDto) { + return ObjectUtils.notEqual(ReflectionUtil.getValue(dto, field), ReflectionUtil.getValue(currentDto, field)); + } } 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 20d38832..fbbaade3 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilter.java @@ -10,7 +10,7 @@ import java.io.IOException; import java.lang.reflect.Field; import java.time.LocalDate; -public class JSonSerializerWithAccessFilter extends JSonAccessFilter { +public class JSonSerializerWithAccessFilter extends JSonAccessFilter { private final JsonGenerator jsonGenerator; private final SerializerProvider serializerProvider; @@ -27,32 +27,35 @@ public class JSonSerializerWithAccessFilter extends JSonAccessFilter { public void serialize() throws IOException { jsonGenerator.writeStartObject(); - for (Field prop : dto.getClass().getDeclaredFields()) { - toJSon(dto, jsonGenerator, prop); + for (Field field : dto.getClass().getDeclaredFields()) { + toJSon(dto, jsonGenerator, field); } jsonGenerator.writeEndObject(); } - private void toJSon(final Object dto, final JsonGenerator jsonGenerator, final Field prop) throws IOException { - if (getLoginUserRole().isAllowedToRead(prop)) { - final String fieldName = prop.getName(); + private void toJSon(final Object dto, final JsonGenerator jsonGenerator, final Field field) throws IOException { + if (getLoginUserRole().isAllowedToRead(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? - if (Integer.class.isAssignableFrom(prop.getType()) || int.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeNumberField(fieldName, (int) get(dto, prop)); - } else if (Long.class.isAssignableFrom(prop.getType()) || long.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeNumberField(fieldName, (long) get(dto, prop)); - } else if (LocalDate.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeStringField(fieldName, get(dto, prop).toString()); // TODO proper format - } else if (Enum.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeStringField(fieldName, get(dto, prop).toString()); // TODO proper representation - } else if (String.class.isAssignableFrom(prop.getType())) { - jsonGenerator.writeStringField(fieldName, (String) get(dto, prop)); + final Object fieldValue = get(dto, field); + if (fieldValue == null) { + jsonGenerator.writeNullField(fieldName); + } 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()); // TODO proper format + } else if (Enum.class.isAssignableFrom(field.getType())) { + jsonGenerator.writeStringField(fieldName, fieldValue.toString()); // TODO proper representation + } else if (String.class.isAssignableFrom(field.getType())) { + jsonGenerator.writeStringField(fieldName, (String) fieldValue); } else { - throw new NotImplementedException("property type not yet implemented: " + prop); + throw new NotImplementedException("property type not yet implemented: " + field); } } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java index 8fbe8c44..e1de24cf 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/Role.java @@ -33,17 +33,12 @@ public enum Role { */ SUPPORTER(3), - /** - * This meta-role is to specify that any kind of customer contact can get access to the resource. - */ - ANY_CUSTOMER_CONTACT(20), - /** * This role is for contractual contacts of a customer, like a director of the company. * Who has this role, has the broadest access to all resources which belong to this customer. * Everything which relates to the contract with the customer, needs this role. */ - CONTRACTUAL_CONTACT(21), + CONTRACTUAL_CONTACT(20), /** * This role is for financial contacts of a customer, e.g. for accessing billing data. @@ -64,6 +59,11 @@ public enum Role { TECHNICAL_CONTACT(22), + /** + * This meta-role is to specify that any kind of customer contact can get access to the resource. + */ + ANY_CUSTOMER_CONTACT(29), + /** * Any user which belongs to a customer has at least this role. */ diff --git a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java index baf95906..6c67b9fb 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/SelfId.java @@ -1,5 +1,7 @@ package org.hostsharing.hsadminng.service.accessfilter; +import org.hostsharing.hsadminng.service.IdToDtoResolver; + import java.lang.annotation.*; /** @@ -14,4 +16,6 @@ import java.lang.annotation.*; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface SelfId { + /// The service which can load the referenced DTO. + Class> resolver(); } diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java index b79ea4f1..d52a535a 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/AssetDTO.java @@ -34,7 +34,7 @@ public class AssetDTO implements Serializable { private Long membershipId; - private String membershipAdmissionDocumentDate; + private String membershipDisplayReference; public Long getId() { return id; @@ -92,12 +92,12 @@ public class AssetDTO implements Serializable { this.membershipId = membershipId; } - public String getMembershipAdmissionDocumentDate() { - return membershipAdmissionDocumentDate; + public String getMembershipDisplayReference() { + return membershipDisplayReference; } - public void setMembershipAdmissionDocumentDate(String membershipAdmissionDocumentDate) { - this.membershipAdmissionDocumentDate = membershipAdmissionDocumentDate; + public void setMembershipDisplayReference(String membershipDisplayReference) { + this.membershipDisplayReference = membershipDisplayReference; } @Override @@ -131,7 +131,7 @@ public class AssetDTO implements Serializable { ", amount=" + getAmount() + ", remark='" + getRemark() + "'" + ", membership=" + getMembershipId() + - ", membership='" + getMembershipAdmissionDocumentDate() + "'" + + ", membership='" + getMembershipDisplayReference() + "'" + "}"; } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java index ceefc0b9..4b9708d1 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/CustomerDTO.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.hostsharing.hsadminng.domain.enumeration.CustomerKind; import org.hostsharing.hsadminng.domain.enumeration.VatRegion; +import org.hostsharing.hsadminng.service.CustomerService; import org.hostsharing.hsadminng.service.accessfilter.*; import org.springframework.boot.jackson.JsonComponent; import org.springframework.context.ApplicationContext; @@ -21,9 +22,9 @@ import java.util.Objects; /** * A DTO for the Customer entity. */ -public class CustomerDTO implements Serializable { +public class CustomerDTO extends FluentBuilder implements Serializable { - @SelfId + @SelfId(resolver = CustomerService.class) @AccessFor(read = Role.ANY_CUSTOMER_USER) private Long id; @@ -41,31 +42,38 @@ public class CustomerDTO implements Serializable { @NotNull @Size(max = 80) - @AccessFor(init = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.ANY_CUSTOMER_USER) private String name; @NotNull + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.CONTRACTUAL_CONTACT) private CustomerKind kind; + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate birthDate; @Size(max = 80) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String birthPlace; @Size(max = 80) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String registrationCourt; @Size(max = 80) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String registrationNumber; @NotNull + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private VatRegion vatRegion; @Size(max = 40) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String vatNumber; @Size(max = 80) - @AccessFor(init = Role.ADMIN, update = Role.CONTRACTUAL_CONTACT, read = Role.ANY_CUSTOMER_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.CONTRACTUAL_CONTACT, read = Role.CONTRACTUAL_CONTACT) private String contractualSalutation; @NotNull @@ -78,7 +86,7 @@ public class CustomerDTO implements Serializable { private String billingSalutation; @Size(max = 400) - @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = Role.CONTRACTUAL_CONTACT) + @AccessFor(init = Role.ADMIN, update = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String billingAddress; @Size(max = 160) diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/FluentBuilder.java b/src/main/java/org/hostsharing/hsadminng/service/dto/FluentBuilder.java new file mode 100644 index 00000000..63cf70c6 --- /dev/null +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/FluentBuilder.java @@ -0,0 +1,15 @@ +package org.hostsharing.hsadminng.service.dto; + +import java.util.function.Consumer; + +public class FluentBuilder { + + @SuppressWarnings("unchecked") + public T with( + Consumer builderFunction) { + builderFunction.accept((T) this); + return (T) this; + } + + +} 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 17b9efb9..2286a064 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/MembershipDTO.java @@ -1,6 +1,7 @@ package org.hostsharing.hsadminng.service.dto; import org.hostsharing.hsadminng.service.CustomerService; +import org.hostsharing.hsadminng.service.MembershipService; import org.hostsharing.hsadminng.service.accessfilter.AccessFor; import org.hostsharing.hsadminng.service.accessfilter.ParentId; import org.hostsharing.hsadminng.service.accessfilter.Role; @@ -11,14 +12,13 @@ import javax.validation.constraints.Size; import java.io.Serializable; import java.time.LocalDate; import java.util.Objects; -import java.util.function.Consumer; /** * A DTO for the Membership entity. */ -public class MembershipDTO implements Serializable { +public class MembershipDTO extends FluentBuilder implements Serializable { - @SelfId + @SelfId(resolver = MembershipService.class) @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long id; @@ -47,12 +47,6 @@ public class MembershipDTO implements Serializable { @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private String customerPrefix; - public MembershipDTO with( - Consumer builderFunction) { - builderFunction.accept(this); - return this; - } - public Long getId() { return id; } diff --git a/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java b/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java index b7d8f54f..cb61e088 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java +++ b/src/main/java/org/hostsharing/hsadminng/service/dto/ShareDTO.java @@ -1,5 +1,13 @@ package org.hostsharing.hsadminng.service.dto; +import org.hostsharing.hsadminng.domain.enumeration.ShareAction; +import org.hostsharing.hsadminng.service.MembershipService; +import org.hostsharing.hsadminng.service.ShareService; +import org.hostsharing.hsadminng.service.accessfilter.AccessFor; +import org.hostsharing.hsadminng.service.accessfilter.ParentId; +import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.accessfilter.SelfId; + import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.Serializable; @@ -11,18 +19,16 @@ import java.util.Objects; */ public class ShareDTO implements Serializable { - @SelfId + @SelfId(resolver = ShareService.class) @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long id; @NotNull @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) - @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate documentDate; @NotNull @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) - @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private LocalDate valueDate; @NotNull @@ -41,8 +47,8 @@ public class ShareDTO implements Serializable { @AccessFor(init = Role.ADMIN, read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) private Long membershipId; - @AccessFor(init = Role.ADMIN, read = Role.SUPPORTER) - private String membershipAdmissionDocumentDate; + @AccessFor(read = {Role.CONTRACTUAL_CONTACT, Role.FINANCIAL_CONTACT}) + private String membershipDisplayReference; public Long getId() { return id; @@ -100,12 +106,12 @@ public class ShareDTO implements Serializable { this.membershipId = membershipId; } - public String getMembershipAdmissionDocumentDate() { - return membershipAdmissionDocumentDate; + public String getMembershipDisplayReference() { + return membershipDisplayReference; } - public void setMembershipAdmissionDocumentDate(String membershipAdmissionDocumentDate) { - this.membershipAdmissionDocumentDate = membershipAdmissionDocumentDate; + public void setMembershipDisplayReference(String membershipDisplayReference) { + this.membershipDisplayReference = membershipDisplayReference; } @Override @@ -139,7 +145,7 @@ public class ShareDTO implements Serializable { ", quantity=" + getQuantity() + ", remark='" + getRemark() + "'" + ", membership=" + getMembershipId() + - ", membership='" + getMembershipAdmissionDocumentDate() + "'" + + ", membership='" + getMembershipDisplayReference() + "'" + "}"; } } diff --git a/src/main/java/org/hostsharing/hsadminng/service/mapper/AssetMapper.java b/src/main/java/org/hostsharing/hsadminng/service/mapper/AssetMapper.java index 1d686822..40399960 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/mapper/AssetMapper.java +++ b/src/main/java/org/hostsharing/hsadminng/service/mapper/AssetMapper.java @@ -12,7 +12,7 @@ import org.mapstruct.Mapping; public interface AssetMapper extends EntityMapper { @Mapping(source = "membership.id", target = "membershipId") - @Mapping(source = "membership.admissionDocumentDate", target = "membershipAdmissionDocumentDate") + @Mapping(source = "membership.admissionDocumentDate", target = "membershipDisplayReference") AssetDTO toDto(Asset asset); @Mapping(source = "membershipId", target = "membership") diff --git a/src/main/java/org/hostsharing/hsadminng/service/mapper/ShareMapper.java b/src/main/java/org/hostsharing/hsadminng/service/mapper/ShareMapper.java index 3f367ce6..775ca7be 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/mapper/ShareMapper.java +++ b/src/main/java/org/hostsharing/hsadminng/service/mapper/ShareMapper.java @@ -12,7 +12,7 @@ import org.mapstruct.Mapping; public interface ShareMapper extends EntityMapper { @Mapping(source = "membership.id", target = "membershipId") - @Mapping(source = "membership.admissionDocumentDate", target = "membershipAdmissionDocumentDate") + @Mapping(source = "membership.admissionDocumentDate", target = "membershipDisplayReference") ShareDTO toDto(Share share); @Mapping(source = "membershipId", target = "membership") diff --git a/src/main/java/org/hostsharing/hsadminng/web/rest/CustomerResource.java b/src/main/java/org/hostsharing/hsadminng/web/rest/CustomerResource.java index aa5e3710..b5a3b74e 100644 --- a/src/main/java/org/hostsharing/hsadminng/web/rest/CustomerResource.java +++ b/src/main/java/org/hostsharing/hsadminng/web/rest/CustomerResource.java @@ -1,25 +1,24 @@ package org.hostsharing.hsadminng.web.rest; + +import io.github.jhipster.web.util.ResponseUtil; +import org.hostsharing.hsadminng.service.CustomerQueryService; import org.hostsharing.hsadminng.service.CustomerService; +import org.hostsharing.hsadminng.service.dto.CustomerCriteria; +import org.hostsharing.hsadminng.service.dto.CustomerDTO; import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; import org.hostsharing.hsadminng.web.rest.util.HeaderUtil; import org.hostsharing.hsadminng.web.rest.util.PaginationUtil; -import org.hostsharing.hsadminng.service.dto.CustomerDTO; -import org.hostsharing.hsadminng.service.dto.CustomerCriteria; -import org.hostsharing.hsadminng.service.CustomerQueryService; -import io.github.jhipster.web.util.ResponseUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.net.URI; import java.net.URISyntaxException; - import java.util.List; import java.util.Optional; @@ -72,7 +71,7 @@ public class CustomerResource { * @throws URISyntaxException if the Location URI syntax is incorrect */ @PutMapping("/customers") - public ResponseEntity updateCustomer(@Valid @RequestBody CustomerDTO customerDTO) throws URISyntaxException { + public ResponseEntity updateCustomer(@RequestBody CustomerDTO customerDTO) { log.debug("REST request to update Customer : {}", customerDTO); if (customerDTO.getId() == null) { throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); diff --git a/src/main/webapp/app/entities/asset/asset-detail.component.html b/src/main/webapp/app/entities/asset/asset-detail.component.html index 93f2e7f5..d31955d3 100644 --- a/src/main/webapp/app/entities/asset/asset-detail.component.html +++ b/src/main/webapp/app/entities/asset/asset-detail.component.html @@ -28,7 +28,7 @@
Membership
diff --git a/src/main/webapp/app/entities/asset/asset.component.html b/src/main/webapp/app/entities/asset/asset.component.html index 2b918714..29ac04a6 100644 --- a/src/main/webapp/app/entities/asset/asset.component.html +++ b/src/main/webapp/app/entities/asset/asset.component.html @@ -20,7 +20,7 @@ Action Amount Remark - Membership + Membership @@ -34,7 +34,7 @@ {{asset.remark}} diff --git a/src/main/webapp/app/entities/share/share-detail.component.html b/src/main/webapp/app/entities/share/share-detail.component.html index bd3df716..d64180ab 100644 --- a/src/main/webapp/app/entities/share/share-detail.component.html +++ b/src/main/webapp/app/entities/share/share-detail.component.html @@ -28,7 +28,7 @@
Membership
diff --git a/src/main/webapp/app/entities/share/share.component.html b/src/main/webapp/app/entities/share/share.component.html index d42d00e1..7a7ba8e6 100644 --- a/src/main/webapp/app/entities/share/share.component.html +++ b/src/main/webapp/app/entities/share/share.component.html @@ -20,7 +20,7 @@ Action Quantity Remark - Membership + Membership @@ -34,7 +34,7 @@ {{share.remark}} diff --git a/src/main/webapp/app/shared/model/asset.model.ts b/src/main/webapp/app/shared/model/asset.model.ts index f5b8f1d1..caac8b37 100644 --- a/src/main/webapp/app/shared/model/asset.model.ts +++ b/src/main/webapp/app/shared/model/asset.model.ts @@ -16,7 +16,7 @@ export interface IAsset { action?: AssetAction; amount?: number; remark?: string; - membershipAdmissionDocumentDate?: string; + membershipDisplayReference?: string; membershipId?: number; } @@ -28,7 +28,7 @@ export class Asset implements IAsset { public action?: AssetAction, public amount?: number, public remark?: string, - public membershipAdmissionDocumentDate?: string, + public membershipDisplayReference?: string, public membershipId?: number ) {} } diff --git a/src/main/webapp/app/shared/model/share.model.ts b/src/main/webapp/app/shared/model/share.model.ts index 215243a7..eade53c3 100644 --- a/src/main/webapp/app/shared/model/share.model.ts +++ b/src/main/webapp/app/shared/model/share.model.ts @@ -12,7 +12,7 @@ export interface IShare { action?: ShareAction; quantity?: number; remark?: string; - membershipAdmissionDocumentDate?: string; + membershipDisplayReference?: string; membershipId?: number; } @@ -24,7 +24,7 @@ export class Share implements IShare { public action?: ShareAction, public quantity?: number, public remark?: string, - public membershipAdmissionDocumentDate?: string, + public membershipDisplayReference?: string, public membershipId?: number ) {} } diff --git a/src/main/webapp/i18n/de/custom-error.json b/src/main/webapp/i18n/de/custom-error.json index 1a6870e2..b82d687c 100644 --- a/src/main/webapp/i18n/de/custom-error.json +++ b/src/main/webapp/i18n/de/custom-error.json @@ -1,5 +1,6 @@ { "error": { + "idNotFound": "Technische Datensatz-ID nicht gefunden", "shareSubscriptionPositiveQuantity": "Zeichnungen von Geschäftsanteilen erfordern eine positive Stückzahl", "shareCancellationNegativeQuantity": "Kündigungen von Geschäftsanteilen erfordern eine negative Stückzahl", "shareTransactionImmutable": "Transaktionen mit Geschäftsanteilen sind unveränderlich", diff --git a/src/main/webapp/i18n/en/custom-error.json b/src/main/webapp/i18n/en/custom-error.json index ee5daa7e..657e0361 100644 --- a/src/main/webapp/i18n/en/custom-error.json +++ b/src/main/webapp/i18n/en/custom-error.json @@ -1,5 +1,6 @@ { "error": { + "idNotFound": "Technical record-ID not found", "shareSubscriptionPositiveQuantity": "Share subscriptions require a positive quantity", "shareCancellationNegativeQuantity": "Share cancellations require a negative quantity", "shareTransactionImmutable": "Share transactions are immutable", diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index da59b241..af508886 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.tuple.ImmutablePair; import org.hostsharing.hsadminng.service.IdToDtoResolver; +import org.hostsharing.hsadminng.service.dto.FluentBuilder; import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; import org.junit.Before; import org.junit.Rule; @@ -13,9 +14,11 @@ 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 static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -31,22 +34,41 @@ public class JSonDeserializerWithAccessFilterUnitTest { public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock - public ApplicationContext ctx; + private ApplicationContext ctx; @Mock - public JsonParser jsonParser; + private AutowireCapableBeanFactory autowireCapableBeanFactory; @Mock - public ObjectCodec codec; + private JsonParser jsonParser; @Mock - public TreeNode treeNode; + private ObjectCodec codec; + + @Mock + private TreeNode treeNode; + + @Mock + private GivenService givenService; + + @Mock + private GivenChildService givenChildService; @Before public void init() { givenAuthenticatedUser(); givenUserHavingRole(GivenDto.class, 1234L, Role.ACTUAL_CUSTOMER_USER); + given (ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); + given(autowireCapableBeanFactory.createBean(GivenService.class)).willReturn(givenService); + given(givenService.findOne(1234L)).willReturn(Optional.of(new GivenDto() + .with(dto -> dto.id = 1234L) + .with(dto -> dto.openIntegerField = 1) + .with(dto -> dto.openLongField = 2L) + .with(dto -> dto.openStringField = "3") + .with(dto -> dto.restrictedField = "initial value of restricted field") + )); + given(jsonParser.getCodec()).willReturn(codec); } @@ -99,21 +121,54 @@ public class JSonDeserializerWithAccessFilterUnitTest { givenUserHavingRole(GivenDto.class, 1234L, Role.FINANCIAL_CONTACT); givenJSonTree(asJSon( ImmutablePair.of("id", 1234L), - ImmutablePair.of("restrictedField", "Restricted String Value"))); + ImmutablePair.of("restrictedField", "update value of restricted field"))); // when GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize(); // then - assertThat(actualDto.restrictedField).isEqualTo("Restricted String Value"); + assertThat(actualDto.restrictedField).isEqualTo("update value of restricted field"); + } + + @Test + public void shouldDeserializeUnchangedStringFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + // given + givenAuthenticatedUser(); + givenUserHavingRole(GivenDto.class, 1234L, Role.ANY_CUSTOMER_USER); + givenJSonTree(asJSon( + ImmutablePair.of("id", 1234L), + ImmutablePair.of("restrictedField", "initial value of restricted field"))); + + // when + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize(); + + // then + assertThat(actualDto.restrictedField).isEqualTo("initial value of restricted field"); + } + + @Test + public void shouldNotDeserializeUpatedStringFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { + // given + givenAuthenticatedUser(); + givenUserHavingRole(GivenDto.class, 1L, Role.ANY_CUSTOMER_USER); + givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "updated value of restricted field"))); + + // when + Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize()); + + // then + assertThat(exception).isInstanceOfSatisfying(BadRequestAlertException.class, badRequestAlertException -> { + assertThat(badRequestAlertException.getParam()).isEqualTo("GivenDto.restrictedField"); + assertThat(badRequestAlertException.getErrorKey()).isEqualTo("initializationProhibited"); + }); } @Test public void shouldInitializeFieldIfRequiredRoleIsNotCoveredByUser() throws IOException { // given givenAuthenticatedUser(); - givenUserHavingRole(null, null, Role.ANY_CUSTOMER_USER); - givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "Restricted String Value"))); + givenUserHavingRole(GivenDto.class, 1L, Role.ANY_CUSTOMER_USER); + givenJSonTree(asJSon(ImmutablePair.of("restrictedField", "another value of restricted field"))); // when Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize()); @@ -130,7 +185,7 @@ public class JSonDeserializerWithAccessFilterUnitTest { // given givenAuthenticatedUser(); givenUserHavingRole(GivenDto.class, 9999L, Role.CONTRACTUAL_CONTACT); - givenJSonTree(asJSon(ImmutablePair.of("parentId", 1111L))); + givenJSonTree(asJSon(ImmutablePair.of("parentId", 1234L))); // when Throwable exception = catchThrowable(() -> new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenChildDto.class).deserialize()); @@ -146,14 +201,14 @@ public class JSonDeserializerWithAccessFilterUnitTest { public void shouldCreateIfRoleRequiredByReferencedEntityIsCoveredByUser() throws IOException { // given givenAuthenticatedUser(); - givenUserHavingRole(GivenDto.class, 1111L, Role.CONTRACTUAL_CONTACT); - givenJSonTree(asJSon(ImmutablePair.of("parentId", 1111L))); + givenUserHavingRole(GivenDto.class, 1234L, Role.CONTRACTUAL_CONTACT); + givenJSonTree(asJSon(ImmutablePair.of("parentId", 1234L))); // when final GivenChildDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenChildDto.class).deserialize(); // then - assertThat(actualDto.parentId).isEqualTo(1111L); + assertThat(actualDto.parentId).isEqualTo(1234L); } @Test @@ -193,9 +248,12 @@ public class JSonDeserializerWithAccessFilterUnitTest { given(codec.readTree(jsonParser)).willReturn(new ObjectMapper().readTree(givenJSon)); } - public static class GivenDto { + abstract class GivenService implements IdToDtoResolver { + } - @SelfId + public static class GivenDto extends FluentBuilder { + + @SelfId(resolver = GivenService.class) @AccessFor(read = Role.ANY_CUSTOMER_USER) Long id; @@ -212,12 +270,12 @@ public class JSonDeserializerWithAccessFilterUnitTest { Long openLongField; } - abstract class GivenService implements IdToDtoResolver { + abstract class GivenChildService implements IdToDtoResolver { } - public static class GivenChildDto { + public static class GivenChildDto extends FluentBuilder { - @SelfId + @SelfId(resolver = GivenChildService.class) @AccessFor(read = Role.ANY_CUSTOMER_USER) Long id; @@ -231,11 +289,11 @@ public class JSonDeserializerWithAccessFilterUnitTest { public static class GivenDtoWithMultipleSelfId { - @SelfId + @SelfId(resolver = GivenChildService.class) @AccessFor(read = Role.ANY_CUSTOMER_USER) Long id; - @SelfId + @SelfId(resolver = GivenChildService.class) @AccessFor(read = Role.ANY_CUSTOMER_USER) Long id2; diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java index f953bcf1..cff8cba4 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonSerializerWithAccessFilterUnitTest.java @@ -5,18 +5,22 @@ import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.hostsharing.hsadminng.service.IdToDtoResolver; +import org.hostsharing.hsadminng.service.dto.FluentBuilder; import org.junit.Before; import org.junit.Rule; 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 static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -26,10 +30,16 @@ public class JSonSerializerWithAccessFilterUnitTest { public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock - public ApplicationContext ctx; + private ApplicationContext ctx; @Mock - public JsonGenerator jsonGenerator; + private AutowireCapableBeanFactory autowireCapableBeanFactory; + + @Mock + private JsonGenerator jsonGenerator; + + @Mock + private GivenCustomerService givenCustomerService; private final GivenDto givenDTO = createSampleDto(); @@ -37,6 +47,12 @@ public class JSonSerializerWithAccessFilterUnitTest { public void init() { MockSecurityContext.givenAuthenticatedUser(); MockSecurityContext.givenUserHavingRole(GivenCustomerDto.class, 888L, Role.ANY_CUSTOMER_USER); + + given(ctx.getAutowireCapableBeanFactory()).willReturn(autowireCapableBeanFactory); + given(autowireCapableBeanFactory.createBean(GivenCustomerService.class)).willReturn(givenCustomerService); + given(givenCustomerService.findOne(888L)).willReturn(Optional.of(new GivenCustomerDto() + .with(dto -> dto.id = 888L) + )); } @Test @@ -84,7 +100,7 @@ public class JSonSerializerWithAccessFilterUnitTest { } class GivenDtoWithUnimplementedFieldType { @AccessFor(read = Role.ANYBODY) - Arbitrary fieldWithUnimplementedType; + Arbitrary fieldWithUnimplementedType = new Arbitrary(); } final GivenDtoWithUnimplementedFieldType givenDtoWithUnimplementedFieldType = new GivenDtoWithUnimplementedFieldType(); @@ -107,8 +123,10 @@ public class JSonSerializerWithAccessFilterUnitTest { return dto; } - private static class GivenCustomerDto { - + private static class GivenCustomerDto extends FluentBuilder { + @SelfId(resolver = GivenService.class) + @AccessFor(read = Role.ANYBODY) + Long id; } private abstract class GivenCustomerService implements IdToDtoResolver { @@ -116,7 +134,12 @@ public class JSonSerializerWithAccessFilterUnitTest { private static class GivenDto { + @SelfId(resolver = GivenService.class) + @AccessFor(read = Role.ANYBODY) + Long id; + @ParentId(resolver = GivenCustomerService.class) + @AccessFor(read = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) Long customerId; @AccessFor(read = {Role.TECHNICAL_CONTACT, Role.FINANCIAL_CONTACT}) @@ -131,4 +154,9 @@ public class JSonSerializerWithAccessFilterUnitTest { @AccessFor(read = Role.ANYBODY) Long openLongField; } + + private abstract class GivenService implements IdToDtoResolver { + } + + } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java index cfa66edf..dab9a8b6 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/MockSecurityContext.java @@ -20,8 +20,13 @@ public class MockSecurityContext { public static void givenUserHavingRole(final Class onClass, final Long onId, final Role role) { if ((onClass == null || onId == null) && !role.isIndependent()) { - throw new IllegalArgumentException("dependent roles like " + role + " depend on DtoClass and ID"); + throw new IllegalArgumentException("dependent roles like " + role + " missing DtoClass and ID"); } SecurityUtils.addUserRole(onClass, onId, role); } + + public static void givenUserHavingRole(final Role role) { + givenUserHavingRole(null, null, role); + } + } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java index d1fe7481..66944774 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/RoleUnitTest.java @@ -28,7 +28,7 @@ public class RoleUnitTest { assertThat(Role.SUPPORTER.covers(Role.ADMIN)).isFalse(); assertThat(Role.ANY_CUSTOMER_CONTACT.covers(Role.SUPPORTER)).isFalse(); - assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.ANY_CUSTOMER_CONTACT)).isFalse(); + assertThat(Role.ANY_CUSTOMER_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isFalse(); assertThat(Role.FINANCIAL_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isFalse(); assertThat(Role.FINANCIAL_CONTACT.covers(Role.TECHNICAL_CONTACT)).isFalse(); assertThat(Role.TECHNICAL_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isFalse(); @@ -55,7 +55,7 @@ public class RoleUnitTest { assertThat(Role.SUPPORTER.covers(Role.ANY_CUSTOMER_CONTACT)).isTrue(); - assertThat(Role.ANY_CUSTOMER_CONTACT.covers(Role.CONTRACTUAL_CONTACT)).isTrue(); + assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.ANY_CUSTOMER_CONTACT)).isTrue(); assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.FINANCIAL_CONTACT)).isTrue(); assertThat(Role.CONTRACTUAL_CONTACT.covers(Role.TECHNICAL_CONTACT)).isTrue(); assertThat(Role.TECHNICAL_CONTACT.covers(Role.ANY_CUSTOMER_USER)).isTrue(); diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java index 34a016ae..5d25cfea 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/CustomerDTOUnitTest.java @@ -2,54 +2,98 @@ package org.hostsharing.hsadminng.service.dto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.hostsharing.hsadminng.domain.Customer; +import org.hostsharing.hsadminng.domain.enumeration.CustomerKind; +import org.hostsharing.hsadminng.domain.enumeration.VatRegion; +import org.hostsharing.hsadminng.repository.CustomerRepository; +import org.hostsharing.hsadminng.service.CustomerService; import org.hostsharing.hsadminng.service.accessfilter.Role; +import org.hostsharing.hsadminng.service.mapper.CustomerMapper; +import org.hostsharing.hsadminng.service.mapper.CustomerMapperImpl; +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; import static org.assertj.core.api.Assertions.assertThat; import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; import static org.junit.Assert.assertEquals; +import static org.mockito.BDDMockito.given; @JsonTest +@SpringBootTest(classes = {CustomerMapperImpl.class, CustomerRepository.class, CustomerService.class, CustomerDTO.CustomerJsonSerializer.class, CustomerDTO.CustomerJsonDeserializer.class}) @RunWith(SpringRunner.class) public class CustomerDTOUnitTest { + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + @Autowired private ObjectMapper objectMapper; + @Autowired + private CustomerMapper customerMapper; + + @MockBean + private CustomerRepository customerRepository; + + @MockBean + private CustomerService customerService; + @Test public void testSerializationAsContractualCustomerContact() throws JsonProcessingException { // given givenAuthenticatedUser(); - givenUserHavingRole(CustomerDTO.class, null, Role.ANY_CUSTOMER_USER); - CustomerDTO given = createSomeCustomerDTO(); + givenUserHavingRole(CustomerDTO.class, 1234L, Role.CONTRACTUAL_CONTACT); + CustomerDTO given = createSomeCustomerDTO(1234L); // when String actual = objectMapper.writeValueAsString(given); // then - given.setContractualAddress(null); - given.setContractualSalutation(null); - given.setBillingAddress(null); - given.setBillingSalutation(null); given.setRemark(null); assertEquals(createExpectedJSon(given), actual); } + @Test + public void testSerializationAsTechnicalCustomerUser() throws JsonProcessingException { + + // given + givenAuthenticatedUser(); + givenUserHavingRole(CustomerDTO.class, 1234L, Role.TECHNICAL_CONTACT); + CustomerDTO given = createSomeCustomerDTO(1234L); + + // when + String actual = objectMapper.writeValueAsString(given); + + // then + final String expectedJSon = "{" + + toJSonFieldDefinition("id", given.getId()) + "," + + toJSonFieldDefinition("reference", given.getReference()) + "," + + toJSonFieldDefinition("prefix", given.getPrefix()) + "," + + toJSonFieldDefinition("name", given.getName()) + + "}"; + assertEquals(expectedJSon, actual); + } + @Test public void testSerializationAsSupporter() throws JsonProcessingException { // given givenAuthenticatedUser(); givenUserHavingRole(CustomerDTO.class, null, Role.SUPPORTER); - CustomerDTO given = createSomeCustomerDTO(); + CustomerDTO given = createSomeCustomerDTO(1234L); // when String actual = objectMapper.writeValueAsString(given); @@ -62,7 +106,8 @@ public class CustomerDTOUnitTest { public void testDeserializeAsContractualCustomerContact() throws IOException { // given givenAuthenticatedUser(); - givenUserHavingRole(CustomerDTO.class, null, Role.CONTRACTUAL_CONTACT); + givenUserHavingRole(CustomerDTO.class, 1234L, Role.CONTRACTUAL_CONTACT); + given(customerRepository.findById(1234L)).willReturn(Optional.of(new Customer().id(1234L))); String json = "{\"id\":1234,\"contractualSalutation\":\"Hallo Updated\",\"billingSalutation\":\"Moin Updated\"}"; // when @@ -84,14 +129,35 @@ public class CustomerDTOUnitTest { toJSonFieldDefinitionIfPresent("reference", dto.getReference()) + toJSonFieldDefinitionIfPresent("prefix", dto.getPrefix()) + toJSonFieldDefinitionIfPresent("name", dto.getName()) + + toJSonFieldDefinitionIfPresent("kind", "LEGAL") + + toJSonNullFieldDefinition("birthDate") + + toJSonNullFieldDefinition("birthPlace") + + toJSonFieldDefinitionIfPresent("registrationCourt", "Registergericht") + + toJSonFieldDefinitionIfPresent("registrationNumber", "Registernummer") + + toJSonFieldDefinitionIfPresent("vatRegion", "DOMESTIC") + + toJSonFieldDefinitionIfPresent("vatNumber", "DE1234") + toJSonFieldDefinitionIfPresent("contractualSalutation", dto.getContractualSalutation()) + toJSonFieldDefinitionIfPresent("contractualAddress", dto.getContractualAddress()) + toJSonFieldDefinitionIfPresent("billingSalutation", dto.getBillingSalutation()) + toJSonFieldDefinitionIfPresent("billingAddress", dto.getBillingAddress()) + - toJSonFieldDefinitionIfPresent("remark", dto.getRemark()) ; + toJSonFieldDefinitionIfPresent("remark", dto.getRemark()); return "{" + json.substring(0, json.length() - 1) + "}"; } + + + private String toJSonFieldDefinition(String name, String value) { + return inQuotes(name) + ":" + (value != null ? inQuotes(value) : "null"); + } + + private String toJSonFieldDefinition(String name, Number value) { + return inQuotes(name) + ":" + (value != null ? value : "null"); + } + + private String toJSonNullFieldDefinition(String name) { + return inQuotes(name) + ":null,"; + } + private String toJSonFieldDefinitionIfPresent(String name, String value) { return value != null ? inQuotes(name) + ":" + inQuotes(value) + "," : ""; } @@ -104,12 +170,17 @@ public class CustomerDTOUnitTest { return "\"" + value.toString() + "\""; } - private CustomerDTO createSomeCustomerDTO() { - CustomerDTO given = new CustomerDTO(); - given.setId(1234L); + private CustomerDTO createSomeCustomerDTO(final long id) { + final CustomerDTO given = new CustomerDTO(); + given.setId(id); given.setReference(10001); given.setPrefix("abc"); given.setName("Mein Name"); + given.setKind(CustomerKind.LEGAL); + given.setRegistrationCourt("Registergericht"); + given.setRegistrationNumber("Registernummer"); + given.setVatRegion(VatRegion.DOMESTIC); + given.setVatNumber("DE1234"); given.setContractualAddress("Eine Adresse"); given.setContractualSalutation("Hallo"); given.setBillingAddress("Noch eine Adresse"); 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 a4976186..90171bed 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/MembershipDTOUnitTest.java @@ -5,6 +5,8 @@ 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.hostsharing.hsadminng.service.CustomerService; +import org.hostsharing.hsadminng.service.MembershipService; import org.hostsharing.hsadminng.service.accessfilter.JSonDeserializerWithAccessFilter; import org.hostsharing.hsadminng.service.accessfilter.Role; import org.hostsharing.hsadminng.web.rest.errors.BadRequestAlertException; @@ -14,9 +16,11 @@ 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 static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -31,20 +35,36 @@ public class MembershipDTOUnitTest { public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock - public ApplicationContext ctx; + private ApplicationContext ctx; @Mock - public JsonParser jsonParser; + private AutowireCapableBeanFactory autowireCapableBeanFactory; @Mock - public ObjectCodec codec; + private JsonParser jsonParser; @Mock - public TreeNode treeNode; + private ObjectCodec codec; + + @Mock + private TreeNode treeNode; + + @Mock + private MembershipService membershipService; + + @Mock + private CustomerService customerService; @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)) + )); } @Test diff --git a/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java index b42e6ac6..b9346faf 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/dto/ShareDTOUnitTest.java @@ -154,7 +154,7 @@ public class ShareDTOUnitTest { givenDTO.setAction(ShareAction.SUBSCRIPTION); givenDTO.setQuantity(3); givenDTO.setDocumentDate(LocalDate.parse("2019-04-22")); - givenDTO.setMembershipDocumentDate("2019-04-21"); // TODO: why is this not a LocalDate? + givenDTO.setMembershipDisplayReference("2019-04-21"); // TODO: why is this not a LocalDate? givenDTO.setValueDate(LocalDate.parse("2019-04-30")); givenDTO.setRemark("Some Remark"); return givenDTO; diff --git a/src/test/java/org/hostsharing/hsadminng/web/rest/CustomerResourceIntTest.java b/src/test/java/org/hostsharing/hsadminng/web/rest/CustomerResourceIntTest.java index 01d0fa92..e63024a2 100644 --- a/src/test/java/org/hostsharing/hsadminng/web/rest/CustomerResourceIntTest.java +++ b/src/test/java/org/hostsharing/hsadminng/web/rest/CustomerResourceIntTest.java @@ -9,6 +9,7 @@ import org.hostsharing.hsadminng.domain.enumeration.VatRegion; import org.hostsharing.hsadminng.repository.CustomerRepository; import org.hostsharing.hsadminng.service.CustomerQueryService; import org.hostsharing.hsadminng.service.CustomerService; +import org.hostsharing.hsadminng.service.accessfilter.Role; import org.hostsharing.hsadminng.service.dto.CustomerDTO; import org.hostsharing.hsadminng.service.mapper.CustomerMapper; import org.hostsharing.hsadminng.web.rest.errors.ExceptionTranslator; @@ -34,9 +35,12 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.hasItem; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenAuthenticatedUser; +import static org.hostsharing.hsadminng.service.accessfilter.MockSecurityContext.givenUserHavingRole; import static org.hostsharing.hsadminng.web.rest.TestUtil.createFormattingConversionService; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + /** * Test class for the CustomerResource REST controller. * @@ -133,6 +137,10 @@ public class CustomerResourceIntTest { @Before public void setup() { MockitoAnnotations.initMocks(this); + + givenAuthenticatedUser(); + givenUserHavingRole(Role.ADMIN); + final CustomerResource customerResource = new CustomerResource(customerService, customerQueryService); this.restCustomerMockMvc = MockMvcBuilders.standaloneSetup(customerResource) .setCustomArgumentResolvers(pageableArgumentResolver) @@ -144,7 +152,7 @@ public class CustomerResourceIntTest { /** * Create an entity for this test. - * + *

* This is a static method, as tests for other entities might also need it, * if they test an entity which requires the current entity. */ @@ -170,7 +178,7 @@ public class CustomerResourceIntTest { /** * Create another entity for tests. - * + *

* This is a static method, as tests for other entities might also need it, * if they test an entity which requires the current entity. */ @@ -372,22 +380,22 @@ public class CustomerResourceIntTest { .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$.[*].id").value(hasItem(customer.getId().intValue()))) .andExpect(jsonPath("$.[*].reference").value(hasItem(DEFAULT_REFERENCE))) - .andExpect(jsonPath("$.[*].prefix").value(hasItem(DEFAULT_PREFIX.toString()))) - .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME.toString()))) + .andExpect(jsonPath("$.[*].prefix").value(hasItem(DEFAULT_PREFIX))) + .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) .andExpect(jsonPath("$.[*].kind").value(hasItem(DEFAULT_KIND.toString()))) .andExpect(jsonPath("$.[*].birthDate").value(hasItem(DEFAULT_BIRTH_DATE.toString()))) - .andExpect(jsonPath("$.[*].birthPlace").value(hasItem(DEFAULT_BIRTH_PLACE.toString()))) - .andExpect(jsonPath("$.[*].registrationCourt").value(hasItem(DEFAULT_REGISTRATION_COURT.toString()))) - .andExpect(jsonPath("$.[*].registrationNumber").value(hasItem(DEFAULT_REGISTRATION_NUMBER.toString()))) + .andExpect(jsonPath("$.[*].birthPlace").value(hasItem(DEFAULT_BIRTH_PLACE))) + .andExpect(jsonPath("$.[*].registrationCourt").value(hasItem(DEFAULT_REGISTRATION_COURT))) + .andExpect(jsonPath("$.[*].registrationNumber").value(hasItem(DEFAULT_REGISTRATION_NUMBER))) .andExpect(jsonPath("$.[*].vatRegion").value(hasItem(DEFAULT_VAT_REGION.toString()))) - .andExpect(jsonPath("$.[*].vatNumber").value(hasItem(DEFAULT_VAT_NUMBER.toString()))) - .andExpect(jsonPath("$.[*].contractualSalutation").value(hasItem(DEFAULT_CONTRACTUAL_SALUTATION.toString()))) - .andExpect(jsonPath("$.[*].contractualAddress").value(hasItem(DEFAULT_CONTRACTUAL_ADDRESS.toString()))) - .andExpect(jsonPath("$.[*].billingSalutation").value(hasItem(DEFAULT_BILLING_SALUTATION.toString()))) - .andExpect(jsonPath("$.[*].billingAddress").value(hasItem(DEFAULT_BILLING_ADDRESS.toString()))) - .andExpect(jsonPath("$.[*].remark").value(hasItem(DEFAULT_REMARK.toString()))); + .andExpect(jsonPath("$.[*].vatNumber").value(hasItem(DEFAULT_VAT_NUMBER))) + .andExpect(jsonPath("$.[*].contractualSalutation").value(hasItem(DEFAULT_CONTRACTUAL_SALUTATION))) + .andExpect(jsonPath("$.[*].contractualAddress").value(hasItem(DEFAULT_CONTRACTUAL_ADDRESS))) + .andExpect(jsonPath("$.[*].billingSalutation").value(hasItem(DEFAULT_BILLING_SALUTATION))) + .andExpect(jsonPath("$.[*].billingAddress").value(hasItem(DEFAULT_BILLING_ADDRESS))) + .andExpect(jsonPath("$.[*].remark").value(hasItem(DEFAULT_REMARK))); } - + @Test @Transactional public void getCustomer() throws Exception { @@ -1162,8 +1170,8 @@ public class CustomerResourceIntTest { // Disconnect from session so that the updates on updatedCustomer are not directly saved in db em.detach(updatedCustomer); updatedCustomer - .reference(UPDATED_REFERENCE) - .prefix(UPDATED_PREFIX) + .reference(null) + .prefix(null) .name(UPDATED_NAME) .kind(UPDATED_KIND) .birthDate(UPDATED_BIRTH_DATE) @@ -1188,8 +1196,8 @@ public class CustomerResourceIntTest { List customerList = customerRepository.findAll(); assertThat(customerList).hasSize(databaseSizeBeforeUpdate); Customer testCustomer = customerList.get(customerList.size() - 1); - assertThat(testCustomer.getReference()).isEqualTo(UPDATED_REFERENCE); - assertThat(testCustomer.getPrefix()).isEqualTo(UPDATED_PREFIX); + assertThat(testCustomer.getReference()).isEqualTo(DEFAULT_REFERENCE); + assertThat(testCustomer.getPrefix()).isEqualTo(DEFAULT_PREFIX); assertThat(testCustomer.getName()).isEqualTo(UPDATED_NAME); assertThat(testCustomer.getKind()).isEqualTo(UPDATED_KIND); assertThat(testCustomer.getBirthDate()).isEqualTo(UPDATED_BIRTH_DATE); From fb3b79cfc413e7d1e72156a9c23949ec3d1412b8 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 25 Apr 2019 12:56:51 +0200 Subject: [PATCH 24/24] for now, make give all users the role of a HOSTMASTER --- .../hostsharing/hsadminng/HsadminNgApp.java | 12 +++++--- .../security/AuthoritiesConstants.java | 4 +++ .../JSonDeserializerWithAccessFilter.java | 28 +++++++++++-------- src/main/resources/.h2.server.properties | 2 +- .../config/liquibase/authorities.csv | 2 ++ .../config/liquibase/users_authorities.csv | 1 + .../service/accessfilter/JSonBuilder.java | 2 +- ...nDeserializerWithAccessFilterUnitTest.java | 14 ++++++++++ 8 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/hostsharing/hsadminng/HsadminNgApp.java b/src/main/java/org/hostsharing/hsadminng/HsadminNgApp.java index d2e0adb8..3b119fde 100644 --- a/src/main/java/org/hostsharing/hsadminng/HsadminNgApp.java +++ b/src/main/java/org/hostsharing/hsadminng/HsadminNgApp.java @@ -1,11 +1,11 @@ package org.hostsharing.hsadminng; +import io.github.jhipster.config.JHipsterConstants; +import org.apache.commons.lang3.StringUtils; import org.hostsharing.hsadminng.config.ApplicationProperties; import org.hostsharing.hsadminng.config.DefaultProfileUtil; - -import io.github.jhipster.config.JHipsterConstants; - -import org.apache.commons.lang3.StringUtils; +import org.hostsharing.hsadminng.security.SecurityUtils; +import org.hostsharing.hsadminng.service.accessfilter.Role; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; @@ -41,6 +41,10 @@ public class HsadminNgApp { */ @PostConstruct public void initApplication() { + + // TODO: remove this hack once proper user roles are implemented + SecurityUtils.addUserRole(null, null, Role.HOSTMASTER); + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) && activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { log.error("You have misconfigured your application! It should not run " + diff --git a/src/main/java/org/hostsharing/hsadminng/security/AuthoritiesConstants.java b/src/main/java/org/hostsharing/hsadminng/security/AuthoritiesConstants.java index 9aebdf89..bb1fd344 100644 --- a/src/main/java/org/hostsharing/hsadminng/security/AuthoritiesConstants.java +++ b/src/main/java/org/hostsharing/hsadminng/security/AuthoritiesConstants.java @@ -5,8 +5,12 @@ package org.hostsharing.hsadminng.security; */ public final class AuthoritiesConstants { + public static final String HOSTMASTER = "ROLE_HOSTMASTER"; + public static final String ADMIN = "ROLE_ADMIN"; + public static final String SUPPORTER = "ROLE_SUPPORTER"; + public static final String USER = "ROLE_USER"; public static final String ANONYMOUS = "ROLE_ANONYMOUS"; 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 de4085ec..0fe113a8 100644 --- a/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java +++ b/src/main/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilter.java @@ -3,10 +3,7 @@ package org.hostsharing.hsadminng.service.accessfilter; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.IntNode; -import com.fasterxml.jackson.databind.node.LongNode; -import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.*; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.ObjectUtils; import org.hostsharing.hsadminng.service.util.ReflectionUtil; @@ -60,7 +57,7 @@ public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { } private void overwriteUnmodifiedFieldsWithCurrentValues(final Object currentDto) { - if ( currentDto == null ) { + if (currentDto == null) { return; } for (Field field : currentDto.getClass().getDeclaredFields()) { @@ -79,21 +76,30 @@ public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { private Object readValue(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(); - } else if (fieldNode instanceof IntNode) { + } + if (fieldNode instanceof IntNode) { return ((IntNode) fieldNode).asInt(); - } else if (fieldNode instanceof LongNode) { + } + if (fieldNode instanceof LongNode) { return ((LongNode) fieldNode).asLong(); - } else if (fieldNode instanceof ArrayNode && LocalDate.class.isAssignableFrom(fieldClass)) { + } + 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()); - } else { + } + { throw new NotImplementedException("property type not yet implemented: " + fieldNode + " -> " + fieldName + ": " + fieldClass); } } private void writeValue(final T dto, final Field field, final Object value) { - if (field.getType().isAssignableFrom(value.getClass())) { + if (value == null) { + ReflectionUtil.setValue(dto, field, null); + } else if (field.getType().isAssignableFrom(value.getClass())) { ReflectionUtil.setValue(dto, field, value); } else if (Integer.class.isAssignableFrom(field.getType()) || int.class.isAssignableFrom(field.getType())) { ReflectionUtil.setValue(dto, field, ((Number) value).intValue()); @@ -121,7 +127,7 @@ public class JSonDeserializerWithAccessFilter extends JSonAccessFilter { throw new BadRequestAlertException("Referencing field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "referencingProhibited"); } } - } else if (isUpdate(field, dto, currentDto) && !getLoginUserRole().isAllowedToUpdate(field)){ + } else if (isUpdate(field, dto, currentDto) && !getLoginUserRole().isAllowedToUpdate(field)) { throw new BadRequestAlertException("Update of field " + toDisplay(field) + " prohibited for current user role " + role, toDisplay(field), "updateProhibited"); } } diff --git a/src/main/resources/.h2.server.properties b/src/main/resources/.h2.server.properties index 909b4938..b775c018 100644 --- a/src/main/resources/.h2.server.properties +++ b/src/main/resources/.h2.server.properties @@ -1,5 +1,5 @@ #H2 Server Properties -#Wed Apr 03 13:36:25 CEST 2019 +#Thu Apr 25 12:42:42 CEST 2019 0=JHipster H2 (Memory)|org.h2.Driver|jdbc\:h2\:mem\:hsadminng|hsadminNg webAllowOthers=true webPort=8082 diff --git a/src/main/resources/config/liquibase/authorities.csv b/src/main/resources/config/liquibase/authorities.csv index af5c6dfa..f56c3aa4 100644 --- a/src/main/resources/config/liquibase/authorities.csv +++ b/src/main/resources/config/liquibase/authorities.csv @@ -1,3 +1,5 @@ name +ROLE_HOSTMASTER ROLE_ADMIN +ROLE_SUPPORTER ROLE_USER diff --git a/src/main/resources/config/liquibase/users_authorities.csv b/src/main/resources/config/liquibase/users_authorities.csv index 06c5feee..bb482bfa 100644 --- a/src/main/resources/config/liquibase/users_authorities.csv +++ b/src/main/resources/config/liquibase/users_authorities.csv @@ -1,4 +1,5 @@ user_id;authority_name +1;ROLE_HOSTMASTER 1;ROLE_ADMIN 1;ROLE_USER 3;ROLE_ADMIN 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 ec1d6487..01e3832f 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonBuilder.java @@ -21,7 +21,7 @@ public class JSonBuilder { } private static String inQuotes(Object value) { - return "\"" + value.toString() + "\""; + return value != null ? "\"" + value.toString() + "\"" : "null"; } } diff --git a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java index af508886..686eff80 100644 --- a/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java +++ b/src/test/java/org/hostsharing/hsadminng/service/accessfilter/JSonDeserializerWithAccessFilterUnitTest.java @@ -72,6 +72,20 @@ public class JSonDeserializerWithAccessFilterUnitTest { given(jsonParser.getCodec()).willReturn(codec); } + @Test + public void shouldDeserializeNullField() throws IOException { + // given + givenJSonTree(asJSon( + ImmutablePair.of("id", 1234L), + ImmutablePair.of("openStringField", null))); + + // when + GivenDto actualDto = new JSonDeserializerWithAccessFilter<>(ctx, jsonParser, null, GivenDto.class).deserialize(); + + // then + assertThat(actualDto.openStringField).isNull(); + } + @Test public void shouldDeserializeStringField() throws IOException { // given