Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

CrazyParanoid
Copy link
Contributor

Added a new authorization method to AuthorizationManager that returns AuthorizationResult.

Closes gh-14843

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 4, 2024
Copy link
Contributor

@jzheaux jzheaux left a 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.

@jzheaux jzheaux self-assigned this Apr 5, 2024
@CrazyParanoid CrazyParanoid force-pushed the gh-14843 branch 3 times, most recently from a368cd1 to 8ee5742 Compare April 7, 2024 15:33
@CrazyParanoid
Copy link
Contributor Author

Hi @jzheaux ! I added the authorize method to the AuthorizationManager and made the default implementation of the check method:

default AuthorizationDecision check(Supplier<Authentication> authentication, T object) {
		return (AuthorizationDecision) authorize(authentication, object);
	}

With this implementation, it will not be possible to leave the tests as is, because in some tests, a mock of the check method is made and its invocation is verified:

verify(this.mockAuthorizationManager).check(any(), any());

And it needs to be changed to authorize, since the mock was made for example:

given(this.mockAuthorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true));

In addition, there are several delegating components that make call check method of delegate. Here you need to mock the authorize and verify its invocation.

@jzheaux jzheaux changed the title Add support AuthorizationResult for AuthorizationManager AuthorizationManager should return AuthorizationResult Apr 17, 2024
Copy link
Contributor

@jzheaux jzheaux left a 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);
Copy link
Contributor

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() {
Copy link
Contributor

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) {
Copy link
Contributor

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) {
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

@jzheaux
Copy link
Contributor

jzheaux commented Sep 17, 2024

@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.

@CrazyParanoid
Copy link
Contributor Author

Hi @jzheaux. Thanks for your feedback! I will complete this issue in the next few days.

@jzheaux jzheaux added in: core An issue in spring-security-core type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Sep 24, 2024
Copy link
Contributor

@jzheaux jzheaux left a 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);
Copy link
Contributor

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() {
Copy link
Contributor

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) {
Copy link
Contributor

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;
Copy link
Contributor

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 {
Copy link
Contributor

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));
Copy link
Contributor

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.

@jzheaux
Copy link
Contributor

jzheaux commented Oct 1, 2024

Additionally, the build appears to be failing due to formatting concerns. Please try running from the command line the following:

./gradlew format && ./gradlew checkstyleMain checkstyleTest

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core An issue in spring-security-core type: enhancement A general enhancement
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

AuthorizationManager should support returning an AuthorizationResult
3 participants