11

Is there any good way to filter JSON output based on Spring Security roles? I'm looking for something like @JsonIgnore, but for role, like @HasRole("ROLE_ADMIN"). How should I implement this?

perak
  • 1,310
  • 5
  • 20
  • 31

3 Answers3

20

For those landing here from Google, here is a similar solution with Spring Boot 1.4.

Define interfaces for each of your roles, e.g.

public class View {
    public interface Anonymous {}

    public interface Guest extends Anonymous {}

    public interface Organizer extends Guest {}

    public interface BusinessAdmin extends Organizer {}

    public interface TechnicalAdmin extends BusinessAdmin {}
}

Declare @JsonView in your entities, e.g.

@Entity
public class SomeEntity {
    @JsonView(View.Anonymous.class)
    String anonymousField;

    @JsonView(View.BusinessAdmin.class)
    String adminField;
}

And define a @ControllerAdvice to pick up the right JsonView based on the roles:

@ControllerAdvice
public class JsonViewConfiguration extends AbstractMappingJacksonResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return super.supports(returnType, converterType);
    }

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
                                           MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {

        Class<?> viewClass = View.Anonymous.class;

        if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) {
            Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();

            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.GUEST.getValue()))) {
                viewClass = View.Guest.class;
            }
            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.ORGANIZER.getValue()))) {
                viewClass = View.Organizer.class;
            }
            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.BUSINESS_ADMIN.getValue()))) {
                viewClass = View.BusinessAdmin.class;
            }
            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.TECHNICAL_ADMIN.getValue()))) {
                viewClass = View.TechnicalAdmin.class;
            }
        }
        bodyContainer.setSerializationView(viewClass);
    }
}
rcomblen
  • 4,579
  • 1
  • 27
  • 32
  • 3
    Worked a treat for me, but note that you may also need to set the spring property `spring.jackson.mapper.default-view-inclusion=true` so you don't have to annotate every single property of your entities – Gavin Clarke Jun 21 '18 at 14:15
  • This is an interesting approach. Please be aware though, that if you're working with Spring Data and define a projection, it will take preference over the view, possibly letting the user sideline the restrictions by passing a projection query parameter to the queries. – Dario Seidl Jul 31 '18 at 14:29
  • Is it possible to have a view which extends from multiple views? What should I do if I want to have a view which has the privilege of View1 and View2? – Cosaic Aug 09 '18 at 14:43
9

Update: The new Answer

you should consider using rkonovalov/jfilter. specially @DynamicFilterComponent helps a lot. you can see a good guide in this DZone article.

@DynamicFilterComponent is explained here.

The old answer

I've just implemented the requirement you've mentioned above. My system uses Restful Jersey 1.17, Spring Security 3.0.7, Jackson 1.9.2. But the solution has nothing to do with Jersey Restful API and you can use it on any other kind of Servlet implementations.

This is the entire 5 steps of my solution:

  1. First you should create an Annotation class for your purpose, Like this:

    JsonSpringView.java

    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface JsonSpringView {
        String springRoles();
    }
    
  2. Then an Annotation Introspector, most of it's Methods should return null, Fill the Methods based on your need, for my requirments i had just used isIgnorableField. Feature is My Implementation For GrantedAuthority interface. Like this:

    JsonSpringViewAnnotationIntrospector.java

    @Component
    public class JsonSpringViewAnnotationIntrospector extends AnnotationIntrospector implements Versioned 
    {
        // SOME METHODS HERE
        @Override
        public boolean isIgnorableField(AnnotatedField)
        {
            if(annotatedField.hasAnnotation(JsonSpringView.class))
            {
                JsonSpringView jsv = annotatedField.getAnnotation(JsonSpringView.class);
                if(jsv.springRoles() != null)
                {
                    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                    if(principal != null && principal instanceof UserDetails)
                    {
                        UserDetails principalUserDetails = (UserDetails) principal;
                        Collection<? extends  GrantedAuthority> authorities = principalUserDetails.getAuthorities();
                        List<String> requiredRoles = Arrays.asList(jsv.springRoles().split(","));
    
                        for(String requiredRole : requiredRoles)
                        {
                            Feature f = new Feature();
                            f.setName(requiredRole);
                            if(authorities.contains(f))
                            // if The Method Have @JsonSpringView Behind it, and Current User has The Required Permission(Feature, Right, ... . Anything You may Name It).
                            return false;
                        }
                        // if The Method Have @JsonSpringView Behind it, but the Current User doesn't have The required Permission(Feature, Right, ... . Anything You may Name It).
                        return true;
                    }
                }
            }
            // if The Method Doesn't Have @JsonSpringView Behind it.
            return false;
        }
    }
    
  3. Jersey servers have a default ObjectMapper for their serialization/deserialization purposes. If you're using such system and you want to change it's default ObjectMapper, Steps 3, 4 and 5 is yours, else you can read this step and your job is done here.

    JsonSpringObjectMapperProvider.java

    @Provider
    public class JsonSpringObjectMapperProvider implements ContextResolver<ObjectMapper>
    {
        ObjectMapper mapper;
    
        public JsonSpringObjectMapperProvider()
        {
            mapper = new ObjectMapper();
            AnnotationIntrospector one = new JsonSpringViewAnnotationIntrospector();
            AnnotationIntrospector two = new JacksonAnnotationIntrospector();
            AnnotationIntrospector three = AnnotationIntrospector.pair(one, two);
    
            mapper.setAnnotationIntrospector(three);
        }
    
        @Override
        public ObjectMapper getContext(Class<?> arg0) {
            return this.mapper;
        }
    }
    
  4. You should extend javax.ws.rs.core.Application and mention Your class Name in Web.xml. Mine is RestApplication.Like this:

    RestApplication.java

    import java.util.HashSet;
    import java.util.Set;
    
    import javax.ws.rs.core.Application;
    
    public class RestApplication extends Application
    {
        public Set<Class<?>> getClasses() 
        {
            Set<Class<?>> classes = new HashSet<Class<?>>();
            classes.add(JsonSpringObjectMapperProvider.class);
            return classes ;
        }
    }
    
  5. and this is the Last Step. you should mention your Application class (from step 4) in your web.xml:

    A part of my web.xml

    <servlet>
        <servlet-name>RestService</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>com.sun.jersey.config.property.package</param-name>
            <param-value>your_restful_resources_package_here</param-value>
        </init-param>
        <init-param>
        <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
            <param-value>true</param-value>
        </init-param>
        <!-- THIS IS THE PART YOU SHOULD PPPAYYY ATTTTENTTTTION TO-->
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>your_package_name_here.RestApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    

and from now on You only need to mention the @JsonSpringView annotation Behind Any Property you want. Like this:

PersonDataTransferObject.java

public class PersonDataTransferObject
{
    private String name;

    @JsonSpringView(springRoles="ADMIN, SUPERUSER")  // Only Admins And Super Users Will See the person National Code in the automatically produced Json.
    private String nationalCode;
}
  • This doesn't work for me , the filter is only applied once with the first user who logs in. Please, can you look at my newer question : http://stackoverflow.com/questions/35558218/jackson-jsonignore-fields-based-on-spring-security-roles . Do you have an updated solution for this issue ? – singe3 Feb 22 '16 at 16:09
  • Please let me know if anyone able to solve this issue? I am also facing same issue i.e., filter is only applied once with first user who logs in. – Raghavendra Mar 16 '16 at 21:25
  • This is awesome, is there a chance to get your example updated for current spring version? – Dimitri Kopriwa Nov 26 '17 at 18:57
  • @BigDong i'm really sorry, currently i'm under heavy pressure to do my own tasks, so cant really update the answer :( – hossein bakhtiari Nov 28 '17 at 14:27
  • That's too bad. This should be implemented by default with spring-security. – Dimitri Kopriwa Nov 28 '17 at 14:59
  • Anyone could share a working project that use this lib, thanks. – nekperu15739 Dec 20 '18 at 10:11
3

Althou it is possible to write custom JSON processing filter (e.g. based on JSON Pointers), it will be a little bit complex to do.

The simplest way is to create your own DTO and map only those properties, which the user is authorized to get.

Community
  • 1
  • 1
Pavel Horal
  • 17,782
  • 3
  • 65
  • 89
  • Won't help if you want to return 10 fields to a user with `admin` role and only 3 fields to a user with `user` role. Unless you have inheritance and check the role inside the endpoint. – prettyvoid Oct 19 '17 at 08:09
  • @prettyvoid I don't understand the comment. Can you elaborate a bit more? The solution I am describing consist of a single DTO class and a property mapper using few IF statements. – Pavel Horal Oct 19 '17 at 11:17
  • 1
    Nevermind sorry for the misleading comment, I thought you're only suggesting a DTO with a constructor that takes the class that have all the fields. But you made it clear when you said a property mapper with a few `if` statements. – prettyvoid Oct 19 '17 at 15:13