1

In a JAX-RS application, some of my resources must be filtered depending on which roles the signed-in user has been assigned to. I'm trying to accomplish this using security annotations (@RolesAllowed, @DenyAll and @PermitAll).

This is what I'm looking for:

public class MyEntity {

  public String getPublicString() {
    ...
  }

  @RolesAllowed("secretRole")
  public String getSecretString() {
    ...
  }
}

@Path("/myResource")
public MyResource {
  @GET @Path("/{id}")
  public MyEntity get(@PathParam("id") int id) {
    ...
  } 
}

Now, everyone (anonymous and logged-in users) can GET MyResource and retrieve MyEntity (per id), but for users in role secretRole, I'd like to see the output (here serialized as JSON):

{
  "publicString": "...",
  "secretString": "..."
}

And other users (either anonymous or otherwise users not acting on role secretRole) should see just:

{
  "publicString": "..."
}

I know Jersey has entity filtering (and an implementation that filters based in security roles).

Unfortunately Liberty (Apache CXF based) has no such feature.


What have I done so far?

Since my solution deals primarily with JSON - using Jackson - I did some work based on Jackson's BeanSerializerModifier. Forget BeanSerializerModifier: it gets called only once per bean type (so the first user defines which properties get serialized for all other users - no, thanks).

Just found another Jackson concept that is applied each time a bean is about to be serialized: PropertyFilter and JsonFilter.

It kind of works, the implementation being very simple:

new SimpleBeanPropertyFilter() {
  @Override
  protected boolean include(BeanPropertyWriter writer) {
    return include((PropertyWriter)writer);
  }

  @Override
  protected boolean include(PropertyWriter writer) {
    if (writer.findAnnotation(DenyAll.class) != null) {
      return false;
    }

    RolesAllowed rolesAllowed = writer.findAnnotation(RolesAllowed.class);
    if (rolesAllowed != null) {
      boolean anyMatch = Arrays.stream(rolesAllowed.value())
        .anyMatch(role -> securityContext.isUserInRole(role));

      if (!anyMatch) {
        return false;
      }
    }

    return true;
  }
}

And what's missing?

The Achilles' heel in above implementation is the securityContext reference (expecting an instance of SecurityContext).

I couldn't find means to get hold of a reference to the current security context.

Usually securityContext is @Context injected - either as a method parameter or as a field parameter. None of this is available to a BeanSerializerModifier.

I've managed to inject @Context SecurityContext (both by field or by constructor parameter); it happens to be a ThreadLocalSecurityContext in Liberty. BUT its method isUserInRole only works for the first request (when the ObjectMapper is created); then the reference gets stale and any other invocation throws NPE (inside isUserInRole method; the securityContext is still a valid java object reference; though referencing a stale object).

What are my constraints?

Jersey is not an option for me. I'm bound to Liberty (which is Apache CXF based).

I'm already used to Jackson, but it is not a must. JSON and REST are.

EDIT

HOLD ON: I thought the problem was the securityContext, but perhaps it is not the culprit. In time: I've managed to inject @Context SecurityContext (both by field or by constructor parameter); it happens to be a ThreadLocalSecurityContext, so I suppose it will get the actual principal from threadlocal storage.

BUT now I realized that BeanSerializerModifier#changeProperties gets called just once (for each bean), then the list of changed properties gets reused! I'll look closely at the Jackson specs; maybe I'll switch to JSON-B, as pointed by @Andy McCright (if its PropertyVisibilityStrategy doesn't also cache the result).

EDIT 2

Previous implementation with BeanSerializerModifier:

public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
  return beanProperties.stream()
    .filter(property -> {
      if (property.findAnnotation(DenyAll.class) != null) {
        return false;
      }

      RolesAllowed rolesAllowed = property.findAnnotation(RolesAllowed.class);
      if (rolesAllowed != null) {
        boolean anyMatch = Arrays.stream(rolesAllowed.value())
          .anyMatch(role -> securityContext.isUserInRole(role));
        if (!anyMatch) {
          return false;
        }
      }

      return true;
    })
    .collect(toList());
}
rslemos
  • 2,454
  • 22
  • 32
  • What version of JAX-RS are you using? If you are using the jaxrs-2.1 feature, you could use JSON-B instead of Jackson. With JAX-RS 2.1 and JSON-B, you can configure a ContextResolver that specifies a custom PropertyVisibilityStrategy that would limit the visibility of fields/methods based on the `@RolesAllowed` annotation. – Andy McCright Jun 12 '20 at 21:33
  • @AndyMcCright I don't get how a `ContextResolver` can help with the `SecurityContext` injection issue. In my quasi-solution with Jackson I had to code a `ContextResolver` that creates an `ObjectMapper`, a singleton used throughout the application. There lies the problem. – rslemos Jun 12 '20 at 23:38
  • Looks like you figured it out! :-) I worked up a similar approach at: https://github.com/andymc12/secureJson - in any case, I'm glad you got it working. – Andy McCright Jun 14 '20 at 03:52
  • Indeed your solution works without the hackish `ThreadLocal` for `SecurityContext` smuggling. When `ContextResolver` constructor has a `@Context Application`, then its product (in this case the `Jsonb`) gets recreated for each request. I don't get why, though. Also, why it is not the case with `@Context SecurityContext`? Also I don't understand how the `Application` object (which I was sure was a singleton) can be injected with `SecurityContext` (which I was sure was request scoped). Either my surenesses are about to be demolished, or we are facing bugs in the specs or in Liberty – rslemos Jun 14 '20 at 13:53
  • I'm fairly certain that the `Application` is a singleton, but the underlying impl of what gets injected is request-aware - so when you add `@Context SecurityContext` to the `Application` subclass, what gets injected is a proxy that redirects to the SecurityContext implementation for that request. What I don't understand is why `@Context SecurityContext` doesn't work when injected directly into the ContextResolver... it seems to me like that should work. If you're happy with what you have, then no worries. If not, you could open an issue at https://github.com/OpenLiberty/open-liberty/issues – Andy McCright Jun 15 '20 at 22:41
  • You are right. I already have my solution with both `@Context SecurityContext` (not working) and `@Context Application` (working). Also I read the specs, and you are right: `SecurityContext` is request bound (the specs even says that `ThreadLocal` is usually employed to make it work). Now I'm trying to create a FAT test case in OpenLiberty codebase so I can open an issue with a better wording. Thanks for pointing it out. – rslemos Jun 15 '20 at 22:58

1 Answers1

0

I've managed to handle an instance of SecurityContext over a ThreadLocal. To this end I've implemented a ContainerRequestFilter:

  static final ThreadLocal<SecurityContext> tlSecurityContext = new ThreadLocal<>();

  @Provider
  public static class SecurityContextSavingRequestFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
      tlSecurityContext.set(requestContext.getSecurityContext());
    }

  }

Then tlSecurityContext.get() can be used as the current SecurityContext.

I don't know, however, if this is invalid or otherwise not recommended by JAX-RS spec.


Beyond this I've also switched to JSON-B (from Jackson) because:

  1. it has better integration with Liberty (both server and client JAX-RS) by means of feature jsonb-1.0;

  2. property filtering is less verbose (than Jackson's PropertyFilter), although less powerful too.

Full solution follows (with comments):

A ContextResolver<Jsonb> to configure Jsonb:

@Provider
public class JsonbConfigContextResolver implements ContextResolver<Jsonb> {

  @Override
  public Jsonb getContext(Class<?> type) {
    return JsonbBuilder.newBuilder().withConfig(getConfig()).build();
  }

  private JsonbConfig getConfig() {
    return new JsonbConfig().withPropertyVisibilityStrategy(new SecurityPropertyVisibilityStrategy());
  }
}

A PropertyVisibilityStrategy to implement filtering proper:

public class SecurityPropertyVisibilityStrategy implements PropertyVisibilityStrategy {
  @Override
  public boolean isVisible(Field field) {
    return false;
  }

  @Override
  public boolean isVisible(Method method) {
    if (method.getAnnotation(DenyAll.class) != null) {
      return false;
    }

    RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
    if (rolesAllowed != null) {
      boolean anyMatch = Arrays.stream(rolesAllowed.value())
          .anyMatch(role -> isUserInRole(role));
      if (!anyMatch) {
        return false;
      }
    }

    return true;
  }

And finally the ThreadLocal hack itself:

  private boolean isUserInRole(String role) {
    return securityContext.get().isUserInRole(role);
  }

  private static final ThreadLocal<SecurityContext> securityContext = new ThreadLocal<>();

  @Provider
  public static class SecurityContextSavingRequestFilter implements ContainerRequestFilter {
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
      securityContext.set(requestContext.getSecurityContext());
    }
  }
}
rslemos
  • 2,454
  • 22
  • 32