-
Notifications
You must be signed in to change notification settings - Fork 5.9k
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
AuthorizationManager should return AuthorizationResult #14846
base: main
Are you sure you want to change the base?
Conversation
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.
Thanks, @CrazyParanoid.
With check
deprecated, we should no longer call it in from other non-deprecated production code in Spring Security. Can you also make those corresponding changes? Please leave tests as-is, though.
Also, please add tests to ensure that the new method works.
Finally, please take a look at AuthorizaitonManagerBeforeMethodInterceptor
and other method interceptors to ensure that they are no longer casting expression values to AuthorizationDecision
. Instead, they should implement the authorize
method, have their check
method call it, and then perform the cast there.
a368cd1
to
8ee5742
Compare
Hi @jzheaux ! I added the
With this implementation, it will not be possible to leave the tests as is, because in some tests, a mock of the
And it needs to be changed to
In addition, there are several delegating components that make call |
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.
Thanks for the updates and for your patience as I got back to this PR, @CrazyParanoid. I've left my feedback inilne.
* @since 6.3 | ||
*/ | ||
@Nullable | ||
AuthorizationResult authorize(Supplier<Authentication> authentication, T object); |
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.
Sorry, I may have misspoken here. We can't introduce a new non-default method to an existing interface as that would not be passive. Will you please switch this so that authorize
has the default implementation?
*/ | ||
public AuthorizationDecision getDecision() { | ||
public AuthorizationResult getDecision() { |
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.
To remain binary compatible, we cannot change return types. Instead, please add a new getter (called getAuthorizationResult
) and deprecate this getter.
*/ | ||
public void setDecision(AuthorizationDecision decision) { | ||
public void setDecision(AuthorizationResult decision) { |
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.
To remain binary compatible, we cannot change return types. Instead, please add a new setter (called setAuthorizationResult
) and deprecate this setter.
@@ -69,16 +69,16 @@ public ObservationAuthorizationManager(ObservationRegistry registry, Authorizati | |||
} | |||
|
|||
@Override | |||
public AuthorizationDecision check(Supplier<Authentication> authentication, T object) { |
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 believe once you change to authorize
being the default method, you will be fine to leave implementations as they are.
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi); | ||
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision); | ||
AuthorizationResult decision = this.authorizationManager.authorize(this::getAuthentication, mi); | ||
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, (AuthorizationDecision) decision); |
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.
Let's consider also adding a publishAuthorizationEvent
implementation that uses AuthorizeResult
.
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 can add a new method:
default <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationResult decision) {
publishAuthorizationEvent(authentication, object,(AuthorizationDecision) decision);
}
Method with AuthorizationDecision
semantics can be declared as deprecated. This will not break backward compatibility with existing AuthorizationEventPublisher
implementations
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.
@jzheaux I suggest adding an AuthorizationEventPublisher
implementation that will not publish events, like NoopAuthorizationEventPublisher
:
public final class NoopAuthorizationEventPublisher implements AuthorizationEventPublisher {
@Override
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object, AuthorizationDecision decision) {
}
@Override
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object, AuthorizationResult decision) {
}
}
AuthorizationEventPublisher
is not marked as a functional interface, but in fact it is one. AuthorizationManagerAfterMethodInterceptor
and AuthorizationFilter
use noPublish
method to implement the publisher. This means that calling this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
will require the AuthorizationDecision
type explicitly. At the same time, we cannot remove the publishAuthorizationEvent
method with AuthorizationDecision
semantic. Therefore, it makes sense to replace eventPublisher
in these components with NoopAuthorizationEventPublisher
.
@CrazyParanoid, also if you have time, once we are aligned on the servlet changes, it would be great if the PR could have the reactive bits as well. I'm happy to add a polish for that if needed. |
Hi @jzheaux. Thanks for your feedback! I will complete this issue in the next few days. |
8ee5742
to
e014a6a
Compare
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.
Looking good, @CrazyParanoid, we're nearly there. I've left some additional feedback inline.
*/ | ||
default <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object, | ||
AuthorizationResult decision) { | ||
publishAuthorizationEvent(authentication, object,(AuthorizationDecision) decision); |
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.
Even though it's a default implementation, we should perform a type-safe check. Maybe you could do the following:
if (!(result instanceof AuthorizationDecision decision)) {
throw new UnsupportedOperationException("result must be of type AuthorizationDecision");
}
publishAuthorizationEvent(authentication, object, decision);
@@ -73,15 +73,32 @@ public T getObject() { | |||
* @return the observed {@link AuthorizationDecision} | |||
*/ | |||
public AuthorizationDecision getDecision() { |
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.
Let's deprecate this as well.
@@ -73,15 +73,32 @@ public T getObject() { | |||
* @return the observed {@link AuthorizationDecision} | |||
*/ | |||
public AuthorizationDecision getDecision() { | |||
return this.decision; | |||
return (AuthorizationDecision) this.authorizationResult; | |||
} | |||
|
|||
/** | |||
* Set the observed {@link AuthorizationDecision} | |||
* @param decision the observed {@link AuthorizationDecision} | |||
*/ | |||
public void setDecision(AuthorizationDecision decision) { |
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.
Let's deprecate this as well
@@ -73,15 +73,32 @@ public T getObject() { | |||
* @return the observed {@link AuthorizationDecision} | |||
*/ | |||
public AuthorizationDecision getDecision() { | |||
return this.decision; | |||
return (AuthorizationDecision) this.authorizationResult; |
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.
Please perform a type check here. The reason is so that the application can give a more informative error than ClassCastException
. Maybe something like:
Assert.isInstanceOf(AuthorizationDecision.class, "please call getAuthorizationResult instead. If you must call getDecision, please ensure that the result you provide is of type AuthorizationDecision");
return (AuthorizationDecision) this.authorizationResult;
* @author Max Batischev | ||
* @since 6.4 | ||
*/ | ||
public final class NoopAuthorizationEventPublisher implements AuthorizationEventPublisher { |
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'd rather not introduce this in this PR. If you are able to make this package-private, I don't mind leaving it. Otherwise, let's stick with private inner classes for now, even though that's a bit of duplication.
@@ -72,7 +72,7 @@ public void beforeWhenMockAuthorizationManagerThenCheck() throws Throwable { | |||
AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor( | |||
Pointcut.TRUE, mockAuthorizationManager); | |||
advice.invoke(mockMethodInvocation); | |||
verify(mockAuthorizationManager).check(any(Supplier.class), eq(mockMethodInvocation)); | |||
verify(mockAuthorizationManager).authorize(any(Supplier.class), eq(mockMethodInvocation)); |
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'd prefer these change to have Mockito call the real authorize
method, thus confirming that folks upgrading can have the same expectations. For example:
given(mockAuthorizationManager.authorize(any(), any()).willCallRealMethod();
Then, this last line in the test shouldn't have to change.
Please see if this strategy works and, if so, apply it to the remaining tests so that their verification steps stay the same.
Additionally, the build appears to be failing due to formatting concerns. Please try running from the command line the following:
This will correct what it can and then give you a report of the things that you need to change manually from a code convention standpoint. |
Added a new authorization method to AuthorizationManager that returns AuthorizationResult.
Closes gh-14843