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 . Forget BeanSerializerModifier
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());
}