Matt R
Matt R

Reputation: 1356

Jersey declarative linking permissions using RolesAllowed

I would like to combine Jersey's DeclarativeLinking feature with its RolesAllowed feature.

I am able to inject links successfully into a response but that injection pays no attention to the RolesAllowed annotation of that endpoint.

As an example, imagine two users. Call them admin and user. Both can access GET resources but only admin may access DELETE.

If a resource method is protected by @RolesAllowed, then I would expect that a user who does not have that role would not have that endpoint injected. That does not appear to be the case.

I have followed the example found here.

Below is a simplified version of the project I'm working on for illustrative purposes.

For admin I would expect the following json,

{
    "id" : 1
    "value" : "someValue",
    "links" : [
        {
            "href": "http://localhost/context/model/1",
            "rel": "self",
            "type": "GET"
        },
        {
            "href": "http://localhost/context/model/1",
            "rel": "delete",
            "type": "DELETE"
        },
    },
}

This is exactly what I get. Unfortunately, I also get this response for user who does not have the correct roles required to access that endpoint. User will still be provided with a link to the delete endpoint, but due to RolesAllowed, that user will receive a 403.

Model

public class Model {

private int id;
private String value;
//constructor/getters/setters

Model representation

public class ModelRep {
    @XmlPath(".") //Allows me to flatten json so that links and document are at the same level
    private Model model;

    @InjectLinks({
        @InjectLink(
            rel = "self",
            type = "GET",
            resource = ModelResource.class,
            method = "getModel",
            bindings = @Binding(name = "id", value = "${instance.id}"),
            style = Style.ABSOLUTE
        ),
        @InjectLink(
            rel = "delete",
            type = "DELETE",
            resource = ModelResource.class,
            method = "deleteModel",
            bindings = @Binding(name = "id", value = "${instance.id}"),
            style = Style.ABSOLUTE
        )
    })
    @XmlJavaTypeAdapter(LinkAdapter.class)
    private List<Link> links;
    //constructor/getters/setters

Model Resource

@Path("/model")
@Produces(MediaType.APPLICATION_JSON)
public class ModelResource {

    @GET
    @Path("/{id}")
    public Response getModel(@PathParam("id") int id) {
        Model m = dao.get(id);
        ModelRep mr = new modelRep(m);
        GenericEntity<ModelRep> ent = new GenericEntity<ModelRep>(mr) {};
        return Response.status(Status.OK).entity(ent);
    }

    @DELETE
    @Path("/{id}")
    @RolesAllowed(Roles.ADMIN)
    public Response deleteModel(@PathParam("id") int id) {
        dao.delete(id);
        return Response.status(Status.NO_CONTENT);
    }

Question Is there some way that conditional declarative linking can be achieved based on RolesAllowed?

I am aware that a condition clause exists for the InjectLink annotation, but the values allowed for that are instance (ModelRep), entity (Response object I think?), resource (ModelResource).

I am not aware of a way to use any of these to check for permissions.

Greatly appreciate any advice

Upvotes: 3

Views: 812

Answers (1)

Matt R
Matt R

Reputation: 1356

So I ended up finding a solution which is a little hacky but works fairly well.

Each resource extends a base resource clase

@Path("/model")
@Produces(MediaType.APPLICATION_JSON)
public class ModelResource extends BaseResource {

BaseResource defines a public method that returns whether or not a particular method for the current resource is restricted with the RolesAllowed annotation.

public abstract class BaseResource {

    @Context
    private SecurityContext security;

    public boolean isMethodRoleProtected(String sMethod) {
        Method[] methods = this.getClass().getDeclaredMethods();
        for (Method m : methods) {
            if (m.getName().equalsIgnoreCase(sMethod)) {
                Annotation[] annotations = m.getAnnotations();
                for (Annotation a : annotations) {
                    if (a instanceof RolesAllowed) {
                        String[] roles = ((RolesAllowed) a).value;
                        return userHasRole(roles);
                    }
                }
            }
        }
        return false;
    }

    protected boolean userHasRole(String... roles) {
        for (String role : roles) {
            if (security.isUserInRole(role)) {
                return true;
            }
        }
        return false;
    }
}

Finally, the InjectLink annotation will look like this,

@InjectLink(
    rel = "self",
    type = "GET",
    resource = SomeResource.class,
    method = "someMethod",
    bindings = @Binding(name = "id", value = "${instance.id}"),
    style = Style.ABSOLUTE,
    condition = "${resource.isMethodRoleProtected(someMethod)}"
),

'resource' is the instance of the current endpoint class which extends BaseResource. Therefore this link will only be injected if the current user has a role which exists in the RolesAllowed annotation of that method.

This solution requires an implementation of SecurityContext.

If anyone else has a better/cleaner solution or a way to improve the one I found, I'd still love to hear and will adjust the accepted answer accordingly. Cheers

Upvotes: 2

Related Questions