Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I implement fine-grained access control in Spring-Data-Rest?

TL;DR: How do I implement fine-grained access control in the flattened REST api approach that Spring-Data-Rest gives us?

So - I'm making an API using Spring-Data-Rest where there's three main access levels:

1) The admin - can see/update all groups

2) An owner of a group - can see/update the group and everything under it

3) An owner of a sub-group - can see/update only his group. No recursive nesting, just one sub-level allowed.

And 'group' is exposed as a resource (has a crud repository).

So far so good - and I've implemented some access control for modification using a Repository Event Handler - so on the create/write/delete side I think I'm fine.

Now I need to get to the point of limiting visibility of some of the items. This is ok for getting a single item since I can use Pre/Post Authorize annotations and reference the principal.

The problem lies in the findAll() methods - I don't have an easy hook to filter out the specific instances I don't want exposed based on the current principal. For example - a sub-group owner could see all groups by doing GET /groups. They should ideally have the items they don't have access to not even be visible at all.

To me this sounds like writing custom @Query() annotations on the repository interfaces, but that doesn't seem doable because:

  • I need to reference the principal in the query. SPeL is supposed to be supported, but doesn't seem to work at all with ?# expressions (despite this blog post suggesting otherwise: https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions). I am using spring-boot with 1.1.8.RELEASE and the Evans-RELEASE train for spring-data generally.

  • The kind of query I need to write is going to be different depending on the access level, which can't realistically be encompassed in a single JPQL statement (if admin select all groups, else get all (sub)groups associated with the principal's user).

Therefore it sounds like I need to write some custom repository implementations for that and just reference the principal in code. Well that's ok - but it seems like a lot of work for each repository that I need to control the access to (I think this will be almost all of them). This applies to findAll and various custom search methods.

Am I approaching this wrong? Is there another approach to dynamically limiting item visibility based on the currently logged-in user that would work better? In a flat namespace like spring-data-rest exposes, I would imagine this would be a common problem.

In a prior design I just solved it by exposing everything under /api/groups/{groupId}/... and had a sub-resource locator act as a single pinch-point to control access to anything under it. No such luck in spring-data-rest.

Update: now stumbling with a custom method overriding findAll() (this works for other methods defined on my custom interface). Though this might be a separate question - I'm blocked right now. Spring-data is just not calling this when I do a GET /groups, but calling the original. Oddly enough it does use my query if I define one on the interface and mark it with @Query (perhaps custom overrides of built-in methods aren't supported anymore?).

public interface GroupRepository extends JpaRepository<Group, Long>, GroupCustomRepository {}


public interface GroupCustomRepository {

    Page<Group> findAll(Pageable pageable);

}

public class GroupCustomRepositoryImpl extends SimpleJpaRepository<Group, Long> implements GroupCustomRepository {

    @Inject
    public GroupCustomRepositoryImpl(EntityManager em) {
        super(Group.class, em);
    }

    @Override
    public Page<Group> findAll(Pageable pageable) {

        MyPrincipal principal = (MyPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        Page<Group> result;
        if (principal.isAdmin()) {
            result = findAll(pageable);
        } else {

            Specification<Group> spec = (root, query, cb) -> cb.or(
                cb.equal(root, principal.getGroup()),
                cb.and(cb.isNotNull(root.get(Group_.parentGroup)), cb.equal(root.get(Group_.parentGroup), principal.getGroup()))
            );

            result = findAll(spec, pageable);
        }

        return result;
    }
}

Update 2: Since I can't access the principal in the @Query, and I can't override it with a custom method, I'm at a brick wall. @PostFilter doesn't work either because the return object is a Page rather than a collection.

I've decided to just wall-off the /groups to admins only, and have everyone else use different approaches (/groups/search/somethingSpecific) with @PostFilters/@PostAuthorizations.

This doesn't seem like it meshes very well with the HAL approach though. Interested in how other people are solving these kinds of issues with Spring-data-rest.

like image 266
Dave LeBlanc Avatar asked Oct 30 '14 00:10

Dave LeBlanc


1 Answers

We ended up approaching this as follows:

  • We created a custom aspect which sits in front of the CRUD methods on a repository. It then looks up and calls an associated 'authorization handler' which is annotated on the repository that dynamically manages authorization details.

  • We had to be pretty heavy-handed when it came to limiting results in a findAll() query (eg: looking at /users) - essentially, only admins could list all of anything sensitive. Otherwise limited users had to use query methods for specific items.

  • We created some reusable authorization-related classes, and use those in certain scenarios - particularly custom queries, eg:

    @PreAuthorize("@authorizations.systemAdminRead()") @Query("select u FROM User r where ...") List findAll();

    @PostAuthorize("@otherAuthorizationHandler.readAllowed(returnObject)") ResponseObject someQuery();

All in all, it works - but it feels very clunky, and it's easy to miss things. I do wish this was baked-in to the framework more, even being able to dynamically adjust the default queries would be useful (when I was attempting this, I wasn't able to have the queries updated appropriately with @Query).

We happen to be using PostgreSQL, so the upcoming row level security (http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/) would have fit the bill nicely, assuming we could feed it the proper authorization details via the DB connection.

like image 56
Dave LeBlanc Avatar answered Sep 29 '22 12:09

Dave LeBlanc