1
0
mirror of synced 2026-08-05 17:57:15 +00:00

Polish Method Authorization Denied Handling

- Renamed @AuthorizationDeniedHandler to @HandleAuthorizationDenied
- Merged the post processor interface into MethodAuthorizationDeniedHandler , it now has two methods handleDeniedInvocation and handleDeniedInvocationResult
- @HandleAuthorizationDenied now handles AuthorizationDeniedException thrown from the method

Issue gh-14601
This commit is contained in:
Marcus Hert Da Coregio
2024-04-09 10:56:40 -03:00
parent 14da8f4fc8
commit 2fbbcc4bd0
31 changed files with 425 additions and 440 deletions
@@ -2256,14 +2256,13 @@ You can also add the Spring Boot property `spring.jackson.default-property-inclu
[[fallback-values-authorization-denied]]
== Providing Fallback Values When Authorization is Denied
There are some scenarios where you may not wish to throw an `AccessDeniedException` when a method is invoked without the required permissions.
Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where access denied happened before invoking the method.
There are some scenarios where you may not wish to throw an `AuthorizationDeniedException` when a method is invoked without the required permissions.
Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where authorization denied happened before invoking the method.
Spring Security provides support for handling and post-processing method access denied by combining {security-api-url}org/springframework/security/authorization/method/AuthorizationDeniedHandler.html[`@AuthorizationDeniedHandler`] with the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> respectively.
Spring Security provides support for handling authorization denied on method invocation by using the {security-api-url}org/springframework/security/authorization/method/HandleAuthorizationDenied.html[`@HandleAuthorizationDenied`].
The handler works for denied authorizations that happened in the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> as well as {security-api-url}org/springframework/security/authorization/AuthorizationDeniedException.html[`AuthorizationDeniedException`] thrown from the method invocation itself.
=== Using with `@PreAuthorize`
Let's consider the example from the <<authorize-object,previous section>>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@AuthorizationDeniedHandler`:
Let's consider the example from the <<authorize-object,previous section>>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@HandleAuthorizationDenied`:
[tabs]
======
@@ -2274,7 +2273,7 @@ Java::
public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return null;
}
@@ -2295,7 +2294,7 @@ public class User {
// ...
@PreAuthorize(value = "hasAuthority('user:read')")
@AuthorizationDeniedHandler(handlerClass = NullMethodAuthorizationDeniedHandler.class)
@HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
public String getEmail() {
return this.email;
}
@@ -2308,7 +2307,7 @@ Kotlin::
----
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { <1>
override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
return null
}
@@ -2325,13 +2324,13 @@ class SecurityConfig {
}
class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3>
class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3>
----
======
<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a `null` value
<2> Register the `NullMethodAuthorizationDeniedHandler` as a bean
<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
<3> Annotate the method with `@HandleAuthorizationDenied` and pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
And then you can verify that a `null` value is returned instead of the `AccessDeniedException`:
@@ -2371,9 +2370,12 @@ fun getEmailWhenProxiedThenNullEmail() {
----
======
=== Using with `@PostAuthorize`
=== Using the Denied Result From the Method Invocation
The same can be achieved with `@PostAuthorize`, however, since `@PostAuthorize` checks are performed after the method is invoked, we have access to the resulting value of the invocation, allowing you to provide fallback values based on the unauthorized results.
There are some scenarios where you might want to return a secure result derived from the denied result.
For example, if a user is not authorized to see email addresses, you might want to apply some masking on the original email address, i.e. _useremail@example.com_ would become _use\\******@example.com_.
For those scenarios, you can override the `handleDeniedInvocationResult` from the `MethodAuthorizationDeniedHandler`, which has the {security-api-url}org/springframework/security/authorization/method/MethodInvocationResult.html[`MethodInvocationResult`] as an argument.
Let's continue with the previous example, but instead of returning `null`, we will return a masked value of the email:
[tabs]
@@ -2382,10 +2384,15 @@ Java::
+
[source,java,role="primary"]
----
public class EmailMaskingMethodAuthorizationDeniedPostProcessor implements MethodAuthorizationDeniedPostProcessor { <1>
public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { <1>
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return "***";
}
@Override
public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
String email = (String) methodInvocationResult.getResult();
return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
}
@@ -2397,8 +2404,8 @@ public class EmailMaskingMethodAuthorizationDeniedPostProcessor implements Metho
public class SecurityConfig {
@Bean <2>
public EmailMaskingMethodAuthorizationDeniedPostProcessor emailMaskingMethodAuthorizationDeniedPostProcessor() {
return new EmailMaskingMethodAuthorizationDeniedPostProcessor();
public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() {
return new EmailMaskingMethodAuthorizationDeniedHandler();
}
}
@@ -2407,7 +2414,7 @@ public class User {
// ...
@PostAuthorize(value = "hasAuthority('user:read')")
@AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor.class)
@HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class)
public String getEmail() {
return this.email;
}
@@ -2418,9 +2425,13 @@ Kotlin::
+
[source,kotlin,role="secondary"]
----
class EmailMaskingMethodAuthorizationDeniedPostProcessor : MethodAuthorizationDeniedPostProcessor {
class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler {
override fun postProcessResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
return "***"
}
override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
val email = methodInvocationResult.result as String
return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
}
@@ -2432,22 +2443,27 @@ class EmailMaskingMethodAuthorizationDeniedPostProcessor : MethodAuthorizationDe
class SecurityConfig {
@Bean
fun emailMaskingMethodAuthorizationDeniedPostProcessor(): EmailMaskingMethodAuthorizationDeniedPostProcessor {
return EmailMaskingMethodAuthorizationDeniedPostProcessor()
fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler {
return EmailMaskingMethodAuthorizationDeniedHandler()
}
}
class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor::class) val email:String) <3>
class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) <3>
----
======
<1> Create an implementation of `MethodAuthorizationDeniedPostProcessor` that returns a masked value of the unauthorized result value
<2> Register the `EmailMaskingMethodAuthorizationDeniedPostProcessor` as a bean
<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `EmailMaskingMethodAuthorizationDeniedPostProcessor` to the `postProcessorClass` attribute
<1> Create an implementation of `MethodAuthorizationDeniedHandler` that returns a masked value of the unauthorized result value
<2> Register the `EmailMaskingMethodAuthorizationDeniedHandler` as a bean
<3> Annotate the method with `@HandleAuthorizationDenied` and pass the `EmailMaskingMethodAuthorizationDeniedHandler` to the `handlerClass` attribute
And then you can verify that a masked email is returned instead of an `AccessDeniedException`:
[WARNING]
====
Since you have access to the original denied value, make sure that you correctly handle it and do not return it to the caller.
====
[tabs]
======
Java::
@@ -2481,20 +2497,20 @@ fun getEmailWhenProxiedThenMaskedEmail() {
----
======
When implementing the `MethodAuthorizationDeniedHandler` or the `MethodAuthorizationDeniedPostProcessor` you have a few options on what you can return:
When implementing the `MethodAuthorizationDeniedHandler` you have a few options on what type you can return:
- A `null` value.
- A non-null value, respecting the method's return type.
- Throw an exception, usually an instance of `AccessDeniedException`. This is the default behavior.
- Throw an exception, usually an instance of `AuthorizationDeniedException`. This is the default behavior.
- A `Mono` type for reactive applications.
Note that since the handler and the post-processor must be registered as beans, you can inject dependencies into them if you need a more complex logic.
Note that since the handler must be registered as beans in your application context, you can inject dependencies into them if you need a more complex logic.
In addition to that, you have available the `MethodInvocation` or the `MethodInvocationResult`, as well as the `AuthorizationResult` for more details related to the authorization decision.
[[deciding-return-based-parameters]]
=== Deciding What to Return Based on Available Parameters
Consider a scenario where there might be multiple mask values for different methods, it would be not so productive if we had to create a handler or post-processor for each of those methods, although it is perfectly fine to do that.
Consider a scenario where there might be multiple mask values for different methods, it would be not so productive if we had to create a handler for each of those methods, although it is perfectly fine to do that.
In such cases, we can use the information passed via parameters to decide what to do.
For example, we can create a custom `@Mask` annotation and a handler that detects that annotation to decide what mask value to return:
@@ -2517,7 +2533,7 @@ public @interface Mask {
public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
return mask.value();
}
@@ -2539,14 +2555,14 @@ public class SecurityConfig {
public class MyService {
@PreAuthorize(value = "hasAuthority('user:read')")
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
@Mask("***")
public String foo() {
return "foo";
}
@PreAuthorize(value = "hasAuthority('user:read')")
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
@Mask("???")
public String bar() {
return "bar";
@@ -2567,7 +2583,7 @@ annotation class Mask(val value: String)
class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {
override fun handle(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
return mask.value
}
@@ -2589,14 +2605,14 @@ class SecurityConfig {
class MyService {
@PreAuthorize(value = "hasAuthority('user:read')")
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
@Mask("***")
fun foo(): String {
return "foo"
}
@PreAuthorize(value = "hasAuthority('user:read')")
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
@Mask("???")
fun bar(): String {
return "bar"
@@ -2653,8 +2669,8 @@ fun barWhenDeniedThenReturnQuestionMarks() {
=== Combining with Meta Annotation Support
You can also combine the `@AuthorizationDeniedHandler` with other annotations in order to reduce and simplify the annotations in a method.
Let's consider the <<deciding-return-based-parameters,example from the previous section>> and merge `@AuthorizationDeniedHandler` with `@Mask`:
You can also combine the `@HandleAuthorizationDenied` with other annotations in order to reduce and simplify the annotations in a method.
Let's consider the <<deciding-return-based-parameters,example from the previous section>> and merge `@HandleAuthorizationDenied` with `@Mask`:
[tabs]
======
@@ -2664,7 +2680,7 @@ Java::
----
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
public @interface Mask {
String value();
@@ -2683,7 +2699,7 @@ Kotlin::
----
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
annotation class Mask(val value: String)
@Mask("***")