Expected behaviour

The ability to configure both PreFilterAuthorizationReactiveMethodInterceptor and PostFilterAuthorizationReactiveMethodInterceptor with the ability to buffer up to a certain number of elements, invoking the SpEL expression only once the buffer threshold has been reached.

Current behaviour

The current implementations of both PreFilterAuthorizationReactiveMethodInterceptor and PostFilterAuthorizationReactiveMethodInterceptor inject a filterWhen() into the reactive pipeline which evaluates whether or not to filter an element of the Flux on a one-by-one basis.

Context

For systems where the authorization determination is made using a database query or external service, the current behaviour results in poor performance because of the number of queries or requests being made.

One option is to implement a custom PreFilterAuthorizationReactiveMethodInterceptor and PostFilterAuthorizationReactiveMethodInterceptor, extending the current default types, but these latter are both final.

Copying their code into new implementations is another option, but requires replicating a slew of other types since many of their supporting classes are package-private.

Comment From: jzheaux

Hi, @jerfer, thanks for the suggestion. I'm not completely clear on how a buffer would work in this case. Since the EvaluationContext changes for each element in the method parameter or return value, it must be evaluated for each element.

That said, I see your point and want a better understanding of your configuration. What is being looked up in the database? Since PreFilterAuthorizationReactiveMethodInterceptor is about evaluating an in-memory expression, I'm not clear on how you are using it.

Comment From: jerfer

Hi @jzheaux, you're very welcome. Before I forget, I think the improvements you've made to Spring Security, especially around authorization in reactive contexts, are great. We've been able to strip out much of the custom AOP code around authorization and convert it to standard Spring Security annotations.

This particular problem stems from the fact that our back-end authorization service can handle bulk filtering. Given a set of IDs and their types (entities), along with the action the user is trying to perform, it can respond with the subset of these IDs on which the user can perform said action.

Being able to buffer (i.e., Flux::buffer) the elements means fewer calls into the authorization service. For us, this service call is over the wire and any chattiness quickly shows up as decreased throughput.

Comment From: jzheaux

I see; you are wanting the published values to be filtered in groups (Flux#buffer) in order to reduce the number of calls to your authorization service. Do I have that right?

The issue is that @PreFilter and @PostFilter are built as predicates, meaning that the expression is intended to perform a test and return a true or false. This doesn't really make sense in the context of running it for a group of items since the return type is wrong (Mono<Boolean> instead of Flux<T>). Folks similarly run into this kind of thing when filtering results from a database instead of defining the filtering logic in a WHERE clause.

More concretely, the expression @PostFilter("@service.check(filterObject)") means "for each element emitted, perform this check" and what you want (I think) is for it to mean "take a batch of elements, check them, and return the ones that passed". Given that, I'm inclined to believe that the annotations aren't a fit for what you want.

I think the best move is to publish your own around @Advice. In that case, you also don't need to go through SpEL but can simply make the invocation to your service directly.

Copying their code into new implementations is another option, but requires replicating a slew of other types since many of their supporting classes are package-private.

A fair amount of that is framework code that you likely won't need. Here is an example of a @PostFilter replacement, @RemotePostFilter (pseudocode) that may provide some guidance in your case:

@Apsect 
@Component
public class FilteringAspect {
    @Around("@annotation(remotePostFilter)")
    public Object around(ProceedingJoinPoint pjp, RemotePostFilter remotePostFilter) throws Throwable {
        Flux<?> result = (Flux<?>) pjp.proceed();
        return result.buffer().map((segment) -> authorizationService.filter(segment))
                .concatMap(Flux::from);
    }
}

If it is feeling like I'm missing something, please feel free to post more detail.

Comment From: jerfer

I see; you are wanting the published values to be filtered in groups (Flux#buffer) in order to reduce the number of calls to your authorization service. Do I have that right?

Yes, you got it right. Since the semantics of the current implementation is completely different than what I need, and given that I already have some aspects defined for filtering which work on Monos containing collections, it should be quick to try your approach and convert these to support Fluxes.

Thanks for the suggestion, much appreciated.