-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Support reactive method security with reactive types #5980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support reactive method security with reactive types #5980
Conversation
I pushed a new commit (& rebased previous one) to bring back the tests that were removed. I changed the expression @PostAuthorize("returnObject?.contains(authentication?.name)") to be @PostAuthorize("@authz.containsAuthenticationName(returnObject, authentication?.name)") and added some methods in the new |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I submitted some more feedback.
I don't really have a lot of time to figure out what my advice is on how to proceed at this point. We cannot accept a breaking change (so we should not modify tests) since we preserve backwards comparability.
What I typically do in these scenarios is take a step back and just add a single test that I need to fix (without modifying existing tests). Then I try and trace through the code where it fails and find a fix that doesn't introduce a breaking change. Often when taking the step back I copy paste snippets of the original code.
I suspect the end solution will need to look at the result of the SpEL expression and see if the type is a Publisher or a boolean and adapt from there (i.e. if a boolean, then just wrap in Mono.just(booleanSpelResult)
)
@@ -47,7 +49,7 @@ public String notPublisherPreAuthorizeFindById(long id) { | |||
} | |||
|
|||
@Override | |||
@PostAuthorize("returnObject?.contains(authentication?.name)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing the tests means we are potentially breaking backwards compatibility. We need to leave tests alone and ensure that any changes that we make don't break the existing tests.
@@ -589,8 +591,8 @@ public DelegatingReactiveMessageService defaultMessageService() { | |||
} | |||
|
|||
@Bean | |||
public Authz authz() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again please leave existing tests as they are. Instead create only new tests that demonstrate the new functionality.
Class<?> returnType = filterTarget.getClass(); | ||
|
||
Function<Object, ? extends Publisher<Boolean>> filter = item -> { | ||
rootObject.setFilterObject(item); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a race condition because each subscription to the publisher (which can happen on any thread) is going to modify the same rootObject
* @param handler The {@link ReactiveMethodSecurityExpressionHandler} | ||
* @since 5.1.2 | ||
*/ | ||
public ExpressionBasedAnnotationAttributeFactory( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot add a reactive interface to an implementation that is used in imperative world because we will get NoClassDefFoundErrors
Thank you @rwinch for the feedback. I'm sure you are busy so your feedback is much appreciated. I think there is a disconnect between my understanding of how the expressions are supposed to behave vs the current implementation on the reactive-side as well as the current implementation on the non-reactive-side, specifically around the use of I'll use the example above here @PostAuthorize("returnObject?.contains(authentication?.name)")
public Flux<String> fluxPostAuthorizeFindById(long id) As things work today before any changes I made, in the above example All the work I did in this PR assumes that If we were to use the same example on the blocking side and write the expression @PostAuthorize("returnObject?.contains(authentication?.name)")
public List<String> listPostAuthorizeFindById(long id) then in that case Based on that, plus the way the documentation is written
I would say that the If the decision is that the behavior of In my opinion I think functionally it should behave the same in both blocking & reactive, which would mean the change being made as part of this PR would not be functionally backwards-compatible in some use cases. Ultimately its your call and I'll adapt to whichever way you want to go with it. If left as-is though, that would mean that the post-expression evaluation would get re-run for each & every item in the returned |
The values are currently being resolved because SpEL doesn't support working with the |
Hello @edeandrea. This PR has been open quite some time without activity, and is currently in conflict. I know you worked quite hard on what is here, but reviewing comments, it doesn't seem like a conclusion was quite reached and quite a bit of additional work would be required it seems. Given all of the above, I'm going to close this PR in a few days, but wanted to give you a heads up first just in case. |
Thank you @sjohnr you are correct it has been quite some time. I do not have plans to resume working on it. |
Thanks for your quick reply. I will go ahead and close this for now. |
From #4841 - copying in last comment from that issue into this PR, while adding some comments. What is in this PR is probably not the final version - there will probably be some re-work, but I want to settle on a few design decisions before re-working a bit:
In my opinion - the tests that were removed don't really make sense for reactive types. Things like
when the return type is a
Mono
/Flux
/Publisher
don't make a whole lot of sense. Spring-el doesn't yet support lambda functions within expressions so calling methods on those types within an expression doesn't make much sense to me.I also added a ton of new functional/integration (see ReactiveMethodSecurityConfigurationTests) tests to cover lots of the scenarios.
I'm unsure of how to "adapt" the old constructor and its arguments (
PreInvocationAuthorizationAdvice
andPostInvocationAuthorizationAdvice
) to work with the new reactive api (ReactivePreInvocationAuthorizationAdvice
andReactivePostInvocationAuthorizationAdvice
).ExpressionUtils
) vs non-blocking (usingReactiveExpressionUtils
))invoke
method needs to change as well in order to work with the reactive streamsWhat I ended up doing was leaving
PrePostAdviceReactiveMethodInterceptor
alone & marking the class itself as deprecated and introducing a newReactivePrePostAdviceMethodInterceptor
class which uses the new reactive pre/post advice implementations.If you have a clearer picture of how to adapt the existing pre/post advice interfaces into the new reactive ones then I'm all ears - I just don't see what they have in common to be able to programmatically adapt the old ones to the new ones.
I made that change because of the way the
MethodInvocation
itself works. In the pre-advice scenario we need to be able to alter the arguments that are "fed" intoMethodInvocation.proceed()
. In the blocking scenario,DefaultMethodSecurityExpressionHandler.filter
actually mutates the arguments in place, assuming the argument is aCollection
. In the reactive world we don't have that luxury - the reactive types are all immutable and can't be modified in place. So in order to get around that we need to be able to supply a different set of arguments completely (i.e. aMono
/Flux
with afilterWhen
attached to it).The only solution I was able to come up with was in
ExpressionBasedReactivePreInvocationAuthorizationAdvice.setArguments
. I thought it best to be able to support all the various implementations ofMethodInvocation
as possible (not justProxyMethodInvocation
). That's why I added thesetArguments
method toSimpleMethodInvocation
.Not to mention all the tests I wrote for testing all this functionality use a sub-class of
SimpleMethodInvocation
, so without it none of my tests will work either.I can refactor a bit and take
SimpleMethodInvocation
out of the picture but then the whole pre-advice will only work correctly if theMethodInvocation
is aProxyMethodInvocation
. I don't know enough about the rest of the codebase to know whether that is acceptable or not.The only other solution I thought about but I feel would be a somewhat larger undertaking would be to change the implementation of the
ReactivePrePostAdviceMethodInterceptor
altogether and use the AspectJ classes (ProceedingJoinPoint
) instead of the Spring AOPMethodInvocation
. From there instead of only having a singleMethodInvocation.proceed()
method, there would be aProceedingJoinPoint.proceed(Object[])
method that we could use to supply different arguments. This would also involve some re-structuring of the way the pre-advice class works as well, since right now it only has a singlebefore
method that returnsMono<Boolean>
. I think we would instead need 2 methods - 1 forbeforeAuthorize
and one forbeforeFilter
.I didn't go that route originally though because I didn't see AspectJ being used anywhere and I wanted to keep the implementation on the reactive side as close as possible to the non-reactive side.
I'm open to ideas though - just wanted to put some context into why I made some of the decisions that I did.