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); - } }