2

I'm unsuccessfully trying to conditionally and dynamically pick which property to serialize to respond to each request with Jersey (using Jackson). The idea behind this is to securely access to properties of objects within a REST API.

I have several objects that I return in API calls that should show/hide fields depending in the user who is authenticated.

For example, lets say I have an object Car

public class Car implements Serializable {

    private Long id;
    private String VIN;
    private String color;

    ...
}

Lets say that if an user with the ROLE_ADMIN is authenticated, all properties should be returned, but if there isn't a logged user only the first two need to be shown.

I was thinking on building something that's annotation based. Something like:

public class Car implements Serializable {

    private Long id;
    private String VIN;

    @Secured({AccessRole.ROLE_ADMIN})
    private String color;

    ...
}

In this case, the color property should only be returned if the access role of the requesting user matches the ones passed via the annotation.

But I'm unable to get a hook on where should I implement this logic.

What I'm trying to implement is a sort of @JsonIgnore but that's conditional and dynamic. All solutions I found so far are static.

Is this even possible?

tomidelucca
  • 2,543
  • 1
  • 26
  • 26

2 Answers2

3

Jersey has support for Entity Filtering. Aside from general filtering, it also supports Role-based Entity Filtering using (javax.annotation.security) annotations.

So you can use the @RolesAllowed, @PermitAll, and @DenyAll annotations on the domain model properties

public static class Model {
    private String secured;

    @RolesAllowed({"ADMIN"})
    public String getSecured() { return this.secured; }
}

To make this work though, you need to have set the SecurityContext inside of a request filter. Jersey will look up the SecurityContext to validate the roles. You can read more about it in this post (Note: the entity filtering is separate from any real authorization that is mentioned in that post. But the post does explain about the SecurityContext).

Basically you will have something like (notice the last line where you set the SecurityContext).

@PreMatching
public static class SimpleAuthFilter implements ContainerRequestFilter {

    private static final Map<String, User> userStore = new ConcurrentHashMap<>();

    static {
        userStore.put("peeskillet", new User("peeskillet", Arrays.asList("ADMIN", "USER")));
        userStore.put("paulski", new User("paulski", Arrays.asList("USER")));
    }

    @Override
    public void filter(ContainerRequestContext request) throws IOException {
        final String authHeader = request.getHeaderString("Authorization");
        final String username = authHeader.split("=")[1];
        final User user = userStore.get(username);
        if (user == null) {
            throw new NotAuthorizedException("No good.");
        }
        request.setSecurityContext(new SimpleSecurityContext(user));
    }
}

Where the SimpleSecurityContext is just a class of your own, where you need to override the isUserInRole method and check if the user has the role

private static class SimpleSecurityContext implements SecurityContext {

    private final User user;

    SimpleSecurityContext(User user) {
        this.user = user;
    }

    @Override
    public Principal getUserPrincipal() {
        return new Principal() {
            @Override
            public String getName() {
                return user.getUsername();
            }
        };
    }

    @Override
    public boolean isUserInRole(String role) {
        return user.getRoles().contains(role);
    }

    @Override
    public boolean isSecure() {
        return false;
    }

    @Override
    public String getAuthenticationScheme() {
        return "simple";
    }
}

That's pretty much it. You will also need to register the SecurityEntityFilteringFeature with the application to make it all work.

See a complete test case in this Gist

Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
0

You can register a custom MessageBodyWriter https://jersey.java.net/documentation/latest/user-guide.html#d0e6951

The MessageBodyWriter will use your custom logic to decide what to write.

It can be done with @JsonView as @dnault suggested. http://www.baeldung.com/jackson-json-view-annotation

Your MessageBodyWriter will hold a jackson mapper and you will apply the writerWithView with the matching view class as described in the above link.

EDIT: see this one - Jackson Json serialization: exclude property respect to the role of the logged user

Community
  • 1
  • 1
galusben
  • 5,948
  • 6
  • 33
  • 52