3

I have some fields in a model that I only want to be returned when the logged in user has the role ROLE_ADMIN. I can use @JsonIgnore but that hides it for everyone. How can I make it hide dynamically?

Tometoyou
  • 7,792
  • 12
  • 62
  • 108
  • 1
    http://stackoverflow.com/questions/11376304/right-way-to-write-json-deserializer-in-spring-or-extend-it .. does this help ? – ArunM Apr 17 '16 at 11:01

2 Answers2

2

You should use Jackson Json Views technology to acheive it - it allows to choose a different set of fields to be serialized programatically. It is also supported by Spring

Consider you have a class Model with two properties: commonField which should be available for everyone and secretField which should be available only for certain users. You should create an hierarchy of views (any classes would work) and specify which field is available in which view using @JsonView annotation

package com.stackoverflow.jsonview;

import com.fasterxml.jackson.annotation.JsonView;

public class Model {

    public static class Public {}
    public static class Secret extends Public {}

    @JsonView(Public.class)
    private String commonField;

    @JsonView(Secret.class)
    private String secretField;

    public Model() {
    }

    public Model(String commonField, String secretField) {
        this.commonField = commonField;
        this.secretField = secretField;
    }

    public String getCommonField() {
        return commonField;
    }

    public void setCommonField(String commonField) {
        this.commonField = commonField;
    }

    public String getSecretField() {
        return secretField;
    }

    public void setSecretField(String secretField) {
        this.secretField = secretField;
    }

}

Now you can specify the view you want to use in concrete ObjectMapper

package com.stackoverflow.jsonview;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;

import static org.junit.Assert.*;

/**
 */
public class ModelTest {

    @Test
    public void testSecretField() throws JsonProcessingException {
        Model model = new Model("commonField","secretField");
        assertEquals("{\"commonField\":\"commonField\",\"secretField\":\"secretField\"}", new ObjectMapper().writerWithView(Model.Secret.class).writeValueAsString(model));
        assertEquals("{\"commonField\":\"commonField\"}", new ObjectMapper().writerWithView(Model.Public.class).writeValueAsString(model));
    }

}

I am not sure if you can use declaratie approach to make spring choose the right view based on user role out of the box, so probably you will have to write some code like this:

@RequestMapping("/data")
public String getData(HttpServletRequest request) {
    Model model = service.getModel();
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper = request.isUserInRole("ROLE_ADMIN") ? objectMapper.writerWithView(Model.Secret.class) : objectMapper.writerWithView(Model.Public.class);
    return objectMapper.writeValueAsString(model);
}
bedrin
  • 4,458
  • 32
  • 53
  • I saw this website, but I couldn't figure out how to actually use it to do what I wanted. Could you provide some sample code that hides a certain property of a model? – Tometoyou Apr 17 '16 at 11:04
  • @Tometoyou I have updated my answer with an example - we are using this pproach in our current project and it proved to be very usefull and convenient – bedrin Apr 17 '16 at 11:32
1

I solved this after literally a full month of trying various things. I'm working with Spring 4.3.1 and boot, with data being returned in Hal using a pagedrepository.

  1. extend RepositoryRestMvcConfiguration as MyRepositoryRestMvcConfiguration and add @Configuration to the class, make sure your starter class has @EnableWebMvc

  2. add this to MyRepositoryRestMvcConfiguration- extend TypeConstrainedMappingJackson2HttpMessageConverter as MyResourceSupportHttpMessageConverter

  3. add this to MyRepositoryRestMvcConfiguration

    @Override
    @Bean
    public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter() {
    
    ArrayList<MediaType> mediaTypes = new ArrayList<MediaType>();
    mediaTypes.add(MediaTypes.HAL_JSON);
    
    if (config().useHalAsDefaultJsonMediaType()) {
        mediaTypes.add(MediaType.APPLICATION_JSON);
    }
    
    int order = config().useHalAsDefaultJsonMediaType() ? Ordered.LOWEST_PRECEDENCE - 10
            : Ordered.LOWEST_PRECEDENCE - 1;
    
    TypeConstrainedMappingJackson2HttpMessageConverter converter = new MyResourceSupportHttpMessageConverter(
            order);
    converter.setObjectMapper(halObjectMapper());
    converter.setSupportedMediaTypes(mediaTypes);
    
    converter.getObjectMapper().addMixIn(Object.class, MyFilteringMixin.class);
    final FilterProvider myRestrictionFilterProvider = new SimpleFilterProvider()
            .addFilter("MyFilteringMixin", new MyPropertyFilter()).setFailOnUnknownId(false);
    converter.getObjectMapper().setFilterProvider(myRestrictionFilterProvider);
    
    return converter;
    }
    
  4. Create an empty Mixin

    package filters;
    import com.fasterxml.jackson.annotation.JsonFilter;
    @JsonFilter("MyFilteringMixin")
    public class MyFilteringMixin {}
    
  5. Create an empty Mixin create class MyPropertyFilter extending SimpleBeanPropertyFilter and override adapt this method serializeAsField(Object, JsonGenerator, SerializerProvider, PropertyWriter)you need to call either super.serializeAsField(pPojo, pJgen, pProvider, pWriter) or pWriter.serializeAsOmittedField(pPojo, pJgen, pProvider) depending on whether you wish to include or discard this particular field.

I added an annotation to the particular fields I wanted to alter and interrogated that annotation when deciding which of these two to call. I injected the security role and stored permitted roles in the annotation.

This alters what Hal shares out to the caller, not what Hal is holding in its repository. Thus you can morph it depending on who the caller is.

bigbadmouse
  • 216
  • 1
  • 11