You have a book REST resource and each book has an owner. Only the owner of the book can access an owned book. JAX-RS specification has no answer to this problem since it only provides a role based security with @RolesAllowed annotation. It is unfortunate JavaEE spec does not offer at last some interfaces which we could then implement for this purpose.. we need to roll our own. There are many ways this can be achieved, I will present one way of doing it.
Owned JPA entities extend a common class
All owned entities should extend a common class, let’s call it OwnedEntity.
@Entity
@Table(name = "books")
public class BookEntity extends OwnedEntity {}
@MappedSuperclass
public class OwnedEntity {
@Nullable
@Column(name="owner_id")
protected String ownerId;
}
Protect owned resources with an interceptor
Create an interceptor which we will use on each owned resource that will check the owner of the entity against the authorized user. We pass the owned entity as a parameter. We will need this information to be able to fetch the correct JPA entity in the interceptor implementation.
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AuthorizeOwner {
@SuppressWarnings("rawtypes")
@Nonbinding Class<? extends OwnedEntity> type() default OwnedEntity.class;
}
We protect an owned resource with this interceptor
@GET
@Path("/{id}")
@AuthorizeOwner(type = BookEntity.class)
public Response findBookById(@PathParam("id") UUID id) {}
Interceptor implementation
@AuthorizeOwner
@Interceptor
@Priority(2001)
public class AuthorizeOwnerInterceptor {
@Context
private SecurityContext sc;
@Inject
private EntityManager em;
@AroundInvoke
public Object methodEntry(InvocationContext ctx) throws Exception {
Annotation t = ctx.getMethod().getAnnotation(AuthorizeOwner.class);
Object[] params = ctx.getParameters();
//ID must be first parameter and of type UUID
if (params.length>0 && params[0] instanceof UUID) {
String id = ((UUID)params[0]).toString();
OwnedEntity object = em.find(((AuthorizeOwner) t).type(), id);
if (object!=null && !object.getOwnerId().equals(sc.getUserPrincipal().getId())) {
throw new ForbiddenException();
}
}
else {
//Illegal use
throw new InternalServerErrorException();
}
return ctx.proceed();
}
}
Make sure the priority of this interceptor is lower than your security interceptor, since a valid authenticated user should already be present before it.
The limitation of this interceptor is that it can only protect ID based resources of type /resource/:id. For list resources, use seperate logic to insert an additional WHERE filter by owner ID to TypedQuery/Criteria query used for list fetching.
Second limitation is that the entity ID should always be declared first in resource method. Another way would be to enforce the name “id” as the parameter name representing the entity ID, but this requires additional reflection info to get method parameter names.
The example here uses SecurityContext to retreive the authorized user. You might need to inject your own context or parsed JWT token to retreive the needed identificator, depending on what you store in your database as owner ID (user UUID, email etc).
An improvement of this interceptor is to check the roles in security context and skip the owner check if role is an ADMIN or similar, since we probably want to allow admins to access all resources.
So how useful is this?
Good:
+protects owned resources with a simple annotation
Not so good:
-only protects ID based resources, you still need a seperate mechanism for lists
-only protects the base entity, not nested owned relations (/book/:id/somethingElse/:id2), which would mean child entity can have different owner than parent and client must be prevented from access of the child. I did not yet stumble upon such a requirement though.
-forcing method parameter position or consistent naming in resource methods