12

I'm attempting to implement fine grain access control while still taking advantage of Spring data rest.

I'm working on securing a CrudRepository so users can only modify or insert data that belongs to them. I'm making use of @PreAuthorize/@PostAuthorize and @PreFilter/@PostFilter to lock access down to the current principal.

So far my repository looks like this.

public interface MyRepository extends CrudRepository<MyObject, Integer> {

    @PreAuthorize("#entity.userId == principal.id")
    @Override
    <S extends MyObject> S save(S entity);

    @PreFilter("filterObject.userId === principal.id")
    @Override
    <S extends MyObject> Iterable<S> save(Iterable<S> entities);

    @PostAuthorize("returnObject.userId == principal.id")
    @Override
    MyObject findOne(Integer integer);

    @PostFilter("filterObject.userId == principal.id")
    @Override
    Iterable<MyObject> findAll();

}

While this is a bit tedious, it does seem to accomplish what I'm after. (If anyone knows a better way, feel free to let me know!)

Where I'm running into problems is with delete(), count() and exists()

    @Override
    long count();

    @Override
    void delete(Integer integer);

    @Override
    void delete(MyObject entity);

    @Override
    void deleteAll();

    @Override
    boolean exists(Integer integer);

These methods either take an Integer ID parameter or none at all. It seems like I would have to first select the entity with the input ID and then perform the auth check.

Is this type of authorization possible within the repository?

Thanks

Edit:

Thanks to ksokol this seems to be working now.

I added a new bean to a @Configuration class

@Bean
public EvaluationContextExtension securityExtension() {
    return new SecurityEvaluationContextExtensionImpl();
}

This bean extends EvaluationContextExtensionSupport and overrides getRootObject to return a SecurityExpressionRoot that holds my custom principal.

public class SecurityEvaluationContextExtensionImpl extends EvaluationContextExtensionSupport {
@Override
public String getExtensionId() {
    return "security";
}

@Override
public Object getRootObject() {
        Authentication authentication =   SecurityContextHolder.getContext().getAuthentication();
        return new SecurityExpressionRoot(authentication){};
    }
}
francis
  • 5,889
  • 3
  • 27
  • 51

2 Answers2

18

As of Spring Security 4.0 you can access security context in Spring Data JPA queries.

Add SecurityEvaluationContextExtension bean to your bean context:

@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
    return new SecurityEvaluationContextExtension();
}

Now you should be able to access Principal in your Spring Data queries:

@Query("select count(m) from MyObject as m where m.user.id = ?#{ principal?.id }")
@Override
long count();

@Modifying
@Query("delete from MyObject as m where m.id = ?1 and m.user.id = ?#{ principal?.id }")
@Override
void delete(Integer integer);

@Modifying
@Query("delete from MyObject as m where m.id = ?1 and m.user.id = ?#{ principal?.id }")
@Override
void delete(MyObject entity);

@Modifying
@Query("delete from MyObject as m where m.user.id = ?#{ principal?.id }")
@Override
void deleteAll();

@Query("select 1 from MyObject as m where m.id = ?1 and m.user.id = ?#{ principal?.id }")
@Override
boolean exists(Integer integer);

Caution. Queries might have errors. I hadn't the time to test it.

ksokol
  • 8,035
  • 3
  • 43
  • 56
  • Thanks ksokol, these JPQL queries will override the default runtime implemented behavior? How are you referencing the method params, are you using ?1 – francis Apr 17 '15 at 12:23
  • 1
    Yes. They override runtime generated queries. By using `?1` etc you are referencing method parameters inside your queries. See [Using @Query](http://docs.spring.io/spring-data/jpa/docs/1.8.0.RELEASE/reference/html/#jpa.query-methods.at-query) for further information. – ksokol Apr 17 '15 at 12:35
3

Can also be achieved by implementing your checks in your custom Spring repository event handlers. See @HandleBeforeCreate, @HandleBeforeUpdate, @HandleBeforeDelete.

Alternatively, you can use permission-based expressions, e.g. with ACL or your custom ones, you can write @PreAuthorize("hasPermission(#id, 'MyObject', 'DELETE')").

aux
  • 1,589
  • 12
  • 20