-
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
Add Authorization Denied Handlers for Method Security #14712
Add Authorization Denied Handlers for Method Security #14712
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 for putting this together, @marcusdacoregio! I've left some feedback inline.
...g/springframework/security/config/annotation/method/configuration/MethodSecurityService.java
Outdated
Show resolved
Hide resolved
...ava/org/springframework/security/authorization/method/MethodAccessDeniedHandlerResolver.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/springframework/security/authorization/method/DeniedHandler.java
Outdated
Show resolved
Hide resolved
...c/main/java/org/springframework/security/authorization/method/MethodAccessDeniedHandler.java
Outdated
Show resolved
Hide resolved
...pringframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java
Outdated
Show resolved
Hide resolved
core/src/main/java/org/springframework/security/authorization/method/DeniedHandler.java
Outdated
Show resolved
Hide resolved
...g/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java
Outdated
Show resolved
Hide resolved
...ava/org/springframework/security/authorization/method/MethodAccessDeniedHandlerResolver.java
Outdated
Show resolved
Hide resolved
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 class has been copied from spring-security-web
. We would probably want to deprecate it in spring-security-web
and recommend usages to use the class from spring-security-core
@rwinch @jzheaux I've pushed a new commit that uses a One concern is that the Another concern is that exceptions can be expensive and it can have a significant impact on application performance (although I haven't done any benchmarking yet). Consider the following class: class User {
// ...
@PreAuthorize("hasRole('ADMIN')")
@HandleAccessDenied(FirstNameFallbackHandler.class)
public String getName() {
return this.name;
}
@PreAuthorize("hasRole('ADMIN')")
@HandleAccessDenied
public String getAddress() {
return this.address;
}
@PreAuthorize("hasRole('ADMIN')")
@HandleAccessDenied
public String getPhoneNumber() {
return this.phoneNumber;
}
} If we were to use those getters, 3 access denied exceptions would have to be constructed (if it is not an admin) just to be handled by |
a7930b5
to
548af57
Compare
After talking to @rwinch and @jzheaux we decided to support filtering non-collection types only in
|
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 @marcusdacoregio This is really taking shape! I've provided some comments below for your consideration
import org.springframework.security.access.AccessDeniedException; | ||
import org.springframework.util.Assert; | ||
|
||
public class DecisionAwareAccessDeniedException extends AccessDeniedException { |
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.
Consider naming this AuthorizationDeniedException
to align with the AuthorizationDecision
contained within it
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.
Or AuthorizationDecisionException
. AuthenticationException
implies that authentication failed, e.g. AuthenticationFailedException
is a little redundant.
|
||
public DecisionAwareAccessDeniedException(String msg, AuthorizationDecision decision) { | ||
super(msg); | ||
Assert.notNull(decision, "decision cannot be null"); |
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.
Should probably verify decision.isGranted() == false
private Object handleOrThrow(MethodInvocation mi, AuthorizationDecision decision) { | ||
MethodAccessDeniedHandler<MethodInvocation> handler = this.deniedHandlerResolver | ||
.resolvePreInvocation(mi.getMethod()); | ||
if (handler == null) { |
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 could change the MethodAccessDeniedHandlerResolver
to always return non-null result. The default implementation would be throw new AccessDeniedException("Access Denied");
private void attemptAuthorization(MethodInvocation mi) { | ||
public void setApplicationContext(ApplicationContext applicationContext) { | ||
Assert.notNull(applicationContext, "applicationContext cannot be null"); | ||
this.deniedHandlerResolver = new DefaultMethodAccessDeniedHandlerResolver(applicationContext); |
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 could be somewhat error prone if we later add a setter method for the deniedHandlerResolver as it might override an explicitly set denied handler. We should probably only set it here if the handler is the default by assigning the default to a static instance and then checking if it is ==).
private static final MethodAccessDeniedHandlerResolver DEFAULT_HANDLER_RESOLVER = ...
private MethodAccessDeniedHandlerResolver deniedHandlerResolver = DEFAULT_HANDLER_RESOLVER;
public void setApplicationContext(ApplicationContext applicationContext) {
// ...
if (this.deniedHandlerResolver == DEFAULT_HANDLER_RESOLVER) {
this.deniedHandlerResolver = new DefaultMethodAccessDeniedHandlerResolver(applicationContext);
}
}
public MethodAccessDeniedHandler<MethodInvocationResult> resolvePostInvocation(Method method) { | ||
PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostAuthorize.class); | ||
if (postAuthorize == null) { | ||
return null; |
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.
In isolation, postAuthorize might return null, but in the context we are using this method, this should never happen because this method is never invoked unless a PostAuthorize was found for the authorization expression to be evaluated. The validation is necessary with this design since this can be invoked in isolation, but it isn't ideal that we know there should be a guarantee to have a non-null PostAuthorize for our use cases, but we must account for it because the APIs are separate.
PreAuthorize is in a very similar situation.
I believe this along with the performance / consistency considerations is a reason to change the design
import org.springframework.security.access.prepost.PreAuthorize; | ||
import org.springframework.util.Assert; | ||
|
||
final class DefaultMethodAccessDeniedHandlerResolver implements MethodAccessDeniedHandlerResolver { |
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.
The way that this is currently implemented the PreAuthorize
and PostAuthorize
annotations are looked up twice. The first is in PreAuthorizeAuthorizationManager
and PostAuthorizeAuthorizationManager
using PreAuthorizeExpressionAttributeRegistry
and PostAuthorizeExpressionAttributeRegistry
respectively. Using additional reflection to lookup methods is unnecessary overhead for something that is likely going to be invoked a lot.
The other concern is that the way in which the annotation is resolved for the MethodAccessDeniedHandler
and the expression is different. I haven't looked through the code, but it is possible that the resolution is inconsistent. If it isn't already inconsistent, it is possible they will become inconsistent.
|
||
import org.aopalliance.intercept.MethodInvocation; | ||
|
||
interface MethodAccessDeniedHandlerResolver { |
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 think that we should remove this interface in favor of encapsulating the logic of looking up the user provided MethodAccessDeniedHandler
in a framework provided MethodAccessDeniedHandler
. The framework provided MethodAccessDeniedHandler
would receive as an argument the information from PreAuthorize
or the PostAuthorize
annotation that was used to create the expression. This means that the annotation only needs to be resolved once (improves performance) and we can guarantee that there is consistency in resolving the expression and the MethodAccessDeniedHandler
to delegate to.
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 do not think that this is being used anymore and I think that it can be removed.
import org.springframework.security.access.AccessDeniedException; | ||
import org.springframework.security.authorization.AuthorizationDecision; | ||
|
||
public interface MethodAccessDeniedHandler<T> { |
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.
Consider the name MethodAuthorizationDecisionHandler
to better align with the argument that is being passed in.
public interface MethodAccessDeniedHandler<T> { | ||
|
||
@Nullable | ||
Object handle(T deniedObject, 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.
Consider a method name that makes it more clear that it can replace the deniedObject. I wonder if this is more of a post processor of sorts in which the class name and the method name could be updated to include postProcess. We'd want the interface documented that it is allowed to throw an AccessDeniedException (likely though it would prefer a DecisionAwareAccessDeniedException
since there is an AuthorizationDecision
available)
d11e4af
to
e11268e
Compare
@rwinch @jzheaux the PR has been updated based on our last discussion.
|
d51006d
to
f2412f4
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.
I've provided feedback below
import org.springframework.security.authorization.AuthorizationException; | ||
import org.springframework.security.authorization.AuthorizationResult; | ||
|
||
public class DefaultPostInvocationAuthorizationDeniedPostProcessor |
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.
Instead of naming it Default
, perhaps consider naming it something around how it behaves.
import org.springframework.security.access.AccessDeniedException; | ||
import org.springframework.util.Assert; | ||
|
||
public class AuthorizationException extends AccessDeniedException { |
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.
Consider naming this AuthorizationDeniedException
to align with AccessDeniedException
and include the information that it used when Authorization was denied.
import org.springframework.security.authorization.AuthorizationResult; | ||
|
||
public class DefaultPostInvocationAuthorizationDeniedPostProcessor | ||
implements AuthorizationDeniedPostProcessor<MethodInvocationResult> { |
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.
If the class were generic, then DefaultPreInvocationAuthorizationDeniedPostProcessor
could be removed. Consider having a static method similar to Collections.empty()
which allows type safe way to provide an implementation of AuthorizationDeniedPostProcessor
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 don’t think the class can be generic because it is used inside the PreAuthorize and PostAuthorize and annotation’s attributes must be constants. We need an actual implementation of the concrete type to use
|
||
import org.aopalliance.intercept.MethodInvocation; | ||
|
||
interface MethodAccessDeniedHandlerResolver { |
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 do not think that this is being used anymore and I think that it can be removed.
9458473
to
4142ce0
Compare
4142ce0
to
6197405
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.
This looks awesome, @marcusdacoregio! I've left some minor feedback inline.
core/src/main/java/org/springframework/security/authorization/AuthorizationDeniedException.java
Outdated
Show resolved
Hide resolved
.../org/springframework/security/authorization/method/PostProcessableAuthorizationDecision.java
Outdated
Show resolved
Hide resolved
...ework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java
Show resolved
Hide resolved
...ava/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java
Show resolved
Hide resolved
...a/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java
Outdated
Show resolved
Hide resolved
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
Outdated
Show resolved
Hide resolved
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
Outdated
Show resolved
Hide resolved
44d8c3e
to
96e3f80
Compare
For this feature, we have at least 2 options:
DeniedHandlerMethodInterceptor
that interceptsAccessDeniedException
thrown from methods annotated with@DeniedHandler
:AuthorizationManagerAfterMethodInterceptor
would have to throw a more contextualAccessDeniedException
, something likeMethodInvocationResultAccessDeniedException
so we can have access to the result object that resulted in an exception to pass it to theMethodAccessDeniedHandler
.@Pre/PostFilter
filters non-collection types, it would have to throw anAccessDeniedException
for not authorized objects instead of just returningnull
soDeniedHandlerMethodInterceptor
can catch it.AuthorizationManagerAfterMethodInterceptor
,AuthorizationManagerBeforeMethodInterceptor
andDefaultMethodSecurityExpressionHandler
:MethodSecurityEvaluationContext
would have to expose theMethod
in order to access it fromDefaultMethodSecurityExpressionHandler
@DeniedHandler
is presentAccessDeniedException
, those classes have access to bothMethodInvocation
and theresult
.