diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java index 7f335a556d..8557b72f20 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java @@ -39,6 +39,7 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.authorization.AuthorizationResult; +import org.springframework.security.authorization.method.AuthorizationDeniedHandler; import org.springframework.security.authorization.method.AuthorizeReturnObject; import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor; @@ -127,54 +128,67 @@ public interface MethodSecurityService { @RequireAdminRole void repeatedAnnotations(); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class) + @PreAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(handlerClass = StarMaskingHandler.class) String preAuthorizeGetCardNumberIfAdmin(String cardNumber); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StartMaskingHandlerChild.class) + @PreAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(handlerClass = StartMaskingHandlerChild.class) String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class) + @PreAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(handlerClass = StarMaskingHandler.class) String preAuthorizeThrowAccessDeniedManually(); - @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = CardNumberMaskingPostProcessor.class) + @PostAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(postProcessorClass = CardNumberMaskingPostProcessor.class) String postAuthorizeGetCardNumberIfAdmin(String cardNumber); - @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class) + @PostAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(postProcessorClass = PostMaskingPostProcessor.class) String postAuthorizeThrowAccessDeniedManually(); - @PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class) + @PreAuthorize("denyAll()") @Mask("methodmask") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationHandler.class) String preAuthorizeDeniedMethodWithMaskAnnotation(); - @PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class) + @PreAuthorize("denyAll()") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationHandler.class) String preAuthorizeDeniedMethodWithNoMaskAnnotation(); @NullDenied(role = "ADMIN") String postAuthorizeDeniedWithNullDenied(); - @PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class) + @PostAuthorize("denyAll()") @Mask("methodmask") + @AuthorizationDeniedHandler(postProcessorClass = MaskAnnotationPostProcessor.class) String postAuthorizeDeniedMethodWithMaskAnnotation(); - @PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class) + @PostAuthorize("denyAll()") + @AuthorizationDeniedHandler(postProcessorClass = MaskAnnotationPostProcessor.class) String postAuthorizeDeniedMethodWithNoMaskAnnotation(); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskAnnotationHandler.class) + @PreAuthorize("hasRole('ADMIN')") @Mask(expression = "@myMasker.getMask()") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationHandler.class) String preAuthorizeWithMaskAnnotationUsingBean(); - @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskAnnotationPostProcessor.class) + @PostAuthorize("hasRole('ADMIN')") @Mask(expression = "@myMasker.getMask(returnObject)") + @AuthorizationDeniedHandler(postProcessorClass = MaskAnnotationPostProcessor.class) String postAuthorizeWithMaskAnnotationUsingBean(); @AuthorizeReturnObject UserRecordWithEmailProtected getUserRecordWithEmailProtected(); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class) + @PreAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(handlerClass = UserFallbackDeniedHandler.class) UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized(); - @PreAuthorize(value = "@authz.checkResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class) - @PostAuthorize(value = "@authz.checkResult(!#result)", + @PreAuthorize("@authz.checkResult(#result)") + @PostAuthorize("@authz.checkResult(!#result)") + @AuthorizationDeniedHandler(handlerClass = MethodAuthorizationDeniedHandler.class, postProcessorClass = MethodAuthorizationDeniedPostProcessor.class) String checkCustomResult(boolean result); @@ -305,7 +319,8 @@ public interface MethodSecurityService { @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited - @PostAuthorize(value = "hasRole('{role}')", postProcessorClass = NullPostProcessor.class) + @PostAuthorize("hasRole('{role}')") + @AuthorizationDeniedHandler(postProcessorClass = NullPostProcessor.class) @interface NullDenied { String role(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java index 66c83348cb..1bdef8f246 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java @@ -33,6 +33,7 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authorization.AuthorizationResult; +import org.springframework.security.authorization.method.AuthorizationDeniedHandler; import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor; import org.springframework.security.authorization.method.MethodInvocationResult; @@ -45,48 +46,60 @@ import org.springframework.util.StringUtils; @ReactiveMethodSecurityService.Mask("classmask") public interface ReactiveMethodSecurityService { - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class) + @PreAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(handlerClass = StarMaskingHandler.class) Mono preAuthorizeGetCardNumberIfAdmin(String cardNumber); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StartMaskingHandlerChild.class) + @PreAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(handlerClass = StartMaskingHandlerChild.class) Mono preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = StarMaskingHandler.class) + @PreAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(handlerClass = StarMaskingHandler.class) Mono preAuthorizeThrowAccessDeniedManually(); - @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = CardNumberMaskingPostProcessor.class) + @PostAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(postProcessorClass = CardNumberMaskingPostProcessor.class) Mono postAuthorizeGetCardNumberIfAdmin(String cardNumber); - @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class) + @PostAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(postProcessorClass = PostMaskingPostProcessor.class) Mono postAuthorizeThrowAccessDeniedManually(); - @PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class) + @PreAuthorize("denyAll()") @Mask("methodmask") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationHandler.class) Mono preAuthorizeDeniedMethodWithMaskAnnotation(); - @PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class) + @PreAuthorize("denyAll()") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationHandler.class) Mono preAuthorizeDeniedMethodWithNoMaskAnnotation(); @NullDenied(role = "ADMIN") Mono postAuthorizeDeniedWithNullDenied(); - @PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class) + @PostAuthorize("denyAll()") @Mask("methodmask") + @AuthorizationDeniedHandler(postProcessorClass = MaskAnnotationPostProcessor.class) Mono postAuthorizeDeniedMethodWithMaskAnnotation(); - @PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class) + @PostAuthorize("denyAll()") + @AuthorizationDeniedHandler(postProcessorClass = MaskAnnotationPostProcessor.class) Mono postAuthorizeDeniedMethodWithNoMaskAnnotation(); - @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskAnnotationHandler.class) + @PreAuthorize("hasRole('ADMIN')") @Mask(expression = "@myMasker.getMask()") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationHandler.class) Mono preAuthorizeWithMaskAnnotationUsingBean(); - @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskAnnotationPostProcessor.class) + @PostAuthorize("hasRole('ADMIN')") @Mask(expression = "@myMasker.getMask(returnObject)") + @AuthorizationDeniedHandler(postProcessorClass = MaskAnnotationPostProcessor.class) Mono postAuthorizeWithMaskAnnotationUsingBean(); - @PreAuthorize(value = "@authz.checkReactiveResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class) - @PostAuthorize(value = "@authz.checkReactiveResult(!#result)", + @PreAuthorize("@authz.checkReactiveResult(#result)") + @PostAuthorize("@authz.checkReactiveResult(!#result)") + @AuthorizationDeniedHandler(handlerClass = MethodAuthorizationDeniedHandler.class, postProcessorClass = MethodAuthorizationDeniedPostProcessor.class) Mono checkCustomResult(boolean result); @@ -217,7 +230,8 @@ public interface ReactiveMethodSecurityService { @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited - @PostAuthorize(value = "hasRole('{value}')", postProcessorClass = NullPostProcessor.class) + @PostAuthorize("hasRole('{value}')") + @AuthorizationDeniedHandler(postProcessorClass = NullPostProcessor.class) @interface NullDenied { String role(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/UserRecordWithEmailProtected.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/UserRecordWithEmailProtected.java index 1fe6bf4651..b3b2e2075a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/UserRecordWithEmailProtected.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/UserRecordWithEmailProtected.java @@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.method.configuration; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.authorization.AuthorizationResult; +import org.springframework.security.authorization.method.AuthorizationDeniedHandler; import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor; import org.springframework.security.authorization.method.MethodInvocationResult; @@ -36,7 +37,8 @@ public class UserRecordWithEmailProtected { return this.name; } - @PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = EmailMaskingPostProcessor.class) + @PostAuthorize("hasRole('ADMIN')") + @AuthorizationDeniedHandler(postProcessorClass = EmailMaskingPostProcessor.class) public String email() { return this.email; } diff --git a/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java b/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java index 306a983309..b9847f8f77 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PostAuthorize.java @@ -23,9 +23,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor; -import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor; - /** * Annotation for specifying a method access-control expression which will be evaluated * after a method has been invoked. @@ -45,10 +42,4 @@ public @interface PostAuthorize { */ String value(); - /** - * @return the {@link MethodAuthorizationDeniedPostProcessor} class used to - * post-process access denied - */ - Class postProcessorClass() default ThrowingMethodAuthorizationDeniedPostProcessor.class; - } diff --git a/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java b/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java index 9f6cd1d1e0..f056eebb16 100644 --- a/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java +++ b/core/src/main/java/org/springframework/security/access/prepost/PreAuthorize.java @@ -23,9 +23,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler; -import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler; - /** * Annotation for specifying a method access-control expression which will be evaluated to * decide whether a method invocation is allowed or not. @@ -45,10 +42,4 @@ public @interface PreAuthorize { */ String value(); - /** - * @return the {@link MethodAuthorizationDeniedHandler} class used to handle access - * denied - */ - Class handlerClass() default ThrowingMethodAuthorizationDeniedHandler.class; - } diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedHandler.java new file mode 100644 index 0000000000..349c84a5cd --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization.method; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for specifying handling behavior when an authorization denied happens in + * method security + * + * @author Marcus da Coregio + * @since 6.3 + * @see org.springframework.security.access.prepost.PreAuthorize + * @see org.springframework.security.access.prepost.PostAuthorize + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface AuthorizationDeniedHandler { + + /** + * The {@link MethodAuthorizationDeniedHandler} used to handle denied authorizations + * from {@link org.springframework.security.access.prepost.PreAuthorize} + * @return + */ + Class handlerClass() default ThrowingMethodAuthorizationDeniedHandler.class; + + /** + * The {@link MethodAuthorizationDeniedPostProcessor} used to post process denied + * authorizations from + * {@link org.springframework.security.access.prepost.PostAuthorize} + * @return + */ + Class postProcessorClass() default ThrowingMethodAuthorizationDeniedPostProcessor.class; + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java index f02d4f9d39..caa739a11c 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java @@ -55,11 +55,24 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA return ExpressionAttribute.NULL_ATTRIBUTE; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(postAuthorize.value()); - MethodAuthorizationDeniedPostProcessor postProcessor = this.postProcessorResolver - .apply(postAuthorize.postProcessorClass()); + MethodAuthorizationDeniedPostProcessor postProcessor = resolvePostProcessor(method, targetClass); return new PostAuthorizeExpressionAttribute(expression, postProcessor); } + private MethodAuthorizationDeniedPostProcessor resolvePostProcessor(Method method, Class targetClass) { + Function lookup = AuthorizationAnnotationUtils + .withDefaults(AuthorizationDeniedHandler.class); + AuthorizationDeniedHandler deniedHandler = lookup.apply(method); + if (deniedHandler != null) { + return this.postProcessorResolver.apply(deniedHandler.postProcessorClass()); + } + deniedHandler = lookup.apply(targetClass(method, targetClass)); + if (deniedHandler != null) { + return this.postProcessorResolver.apply(deniedHandler.postProcessorClass()); + } + return this.defaultPostProcessor; + } + private PostAuthorize findPostAuthorizeAnnotation(Method method, Class targetClass) { Function lookup = findUniqueAnnotation(PostAuthorize.class); PostAuthorize postAuthorize = lookup.apply(method); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java index 6f89c72699..a224dfcb1b 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java @@ -55,10 +55,24 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt return ExpressionAttribute.NULL_ATTRIBUTE; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(preAuthorize.value()); - MethodAuthorizationDeniedHandler handler = this.handlerResolver.apply(preAuthorize.handlerClass()); + MethodAuthorizationDeniedHandler handler = resolveHandler(method, targetClass); return new PreAuthorizeExpressionAttribute(expression, handler); } + private MethodAuthorizationDeniedHandler resolveHandler(Method method, Class targetClass) { + Function lookup = AuthorizationAnnotationUtils + .withDefaults(AuthorizationDeniedHandler.class); + AuthorizationDeniedHandler deniedHandler = lookup.apply(method); + if (deniedHandler != null) { + return this.handlerResolver.apply(deniedHandler.handlerClass()); + } + deniedHandler = lookup.apply(targetClass(method, targetClass)); + if (deniedHandler != null) { + return this.handlerResolver.apply(deniedHandler.handlerClass()); + } + return this.defaultHandler; + } + private PreAuthorize findPreAuthorizeAnnotation(Method method, Class targetClass) { Function lookup = findUniqueAnnotation(PreAuthorize.class); PreAuthorize preAuthorize = lookup.apply(method); diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 2a6be2454d..0dbc4153de 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -2251,12 +2251,11 @@ You can also add the Spring Boot property `spring.jackson.default-property-inclu 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. -Spring Security provides support for handling and post-processing method access denied with the <> respectively. -The `@PreAuthorize` annotation works with implementations of `MethodAuthorizationDeniedHandler` while the `@PostAuthorize` annotation works with implementations of `MethodAuthorizationDeniedPostProcessor`. +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 <> respectively. === Using with `@PreAuthorize` -Let's consider the example from the <>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@PreAuthorize`: +Let's consider the example from the <>, but instead of creating the `AccessDeniedExceptionInterceptor` to transform an `AccessDeniedException` to a `null` return value, we will use the `handlerClass` attribute from `@AuthorizationDeniedHandler`: [tabs] ====== @@ -2287,7 +2286,8 @@ public class SecurityConfig { public class User { // ... - @PreAuthorize(value = "hasAuthority('user:read')", handlerClass = NullMethodAuthorizationDeniedHandler.class) + @PreAuthorize(value = "hasAuthority('user:read')") + @AuthorizationDeniedHandler(handlerClass = NullMethodAuthorizationDeniedHandler.class) public String getEmail() { return this.email; } @@ -2317,16 +2317,21 @@ class SecurityConfig { } -class User (val name:String, @get:PreAuthorize(value = "hasAuthority('user:read')", handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) <3> +class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(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> Pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute of `@PreAuthorize` +<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `NullMethodAuthorizationDeniedHandler` to the `handlerClass` attribute And then you can verify that a `null` value is returned instead of the `AccessDeniedException`: +[TIP] +==== +You can also annotate your class with `@Component` instead of creating a `@Bean` method +==== + [tabs] ====== Java:: @@ -2393,7 +2398,8 @@ public class SecurityConfig { public class User { // ... - @PostAuthorize(value = "hasAuthority('user:read')", postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor.class) + @PostAuthorize(value = "hasAuthority('user:read')") + @AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor.class) public String getEmail() { return this.email; } @@ -2424,13 +2430,13 @@ class SecurityConfig { } -class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')", postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor::class) val email:String) <3> +class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @AuthorizationDeniedHandler(postProcessorClass = EmailMaskingMethodAuthorizationDeniedPostProcessor::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> Pass the `EmailMaskingMethodAuthorizationDeniedPostProcessor` to the `postProcessorClass` attribute of `@PostAuthorize` +<3> Annotate the method with `@AuthorizationDeniedHandler` and pass the `EmailMaskingMethodAuthorizationDeniedPostProcessor` to the `postProcessorClass` attribute And then you can verify that a masked email is returned instead of an `AccessDeniedException`: @@ -2477,9 +2483,10 @@ When implementing the `MethodAuthorizationDeniedHandler` or the `MethodAuthoriza 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. 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 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 or post-processor 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: @@ -2523,13 +2530,15 @@ public class SecurityConfig { @Component public class MyService { - @PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler.class) + @PreAuthorize(value = "hasAuthority('user:read')") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class) @Mask("***") public String foo() { return "foo"; } - @PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler.class) + @PreAuthorize(value = "hasAuthority('user:read')") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class) @Mask("???") public String bar() { return "bar"; @@ -2571,13 +2580,15 @@ class SecurityConfig { @Component class MyService { - @PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler::class) + @PreAuthorize(value = "hasAuthority('user:read')") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class) @Mask("***") fun foo(): String { return "foo" } - @PreAuthorize(value = "hasAuthority('user:read')", handlerClass = MaskAnnotationDeniedHandler::class) + @PreAuthorize(value = "hasAuthority('user:read')") + @AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class) @Mask("???") fun bar(): String { return "bar" @@ -2634,34 +2645,8 @@ fun barWhenDeniedThenReturnQuestionMarks() { === Combining with Meta Annotation Support -Some authorization expressions may be long enough that it can become hard to read or to maintain. -For example, consider the following `@PreAuthorize` expression: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler.class) -public String myMethod() { - // ... -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler::class) -fun myMethod(): String { - // ... -} ----- -====== - -The way it is, it is somewhat hard to read it, but we can do better. -By using the <>, we can simplify it to: +You can also combine the `@AuthorizationDeniedHandler` with other annotations in order to reduce and simplify the annotations in a method. +Let's consider the <> and merge `@AuthorizationDeniedHandler` with `@Mask`: [tabs] ====== @@ -2671,10 +2656,14 @@ Java:: ---- @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler.class) -public @interface NullDenied {} +@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler.class) +public @interface Mask { -@NullDenied + String value(); + +} + +@Mask("***") public String myMethod() { // ... } @@ -2686,16 +2675,17 @@ Kotlin:: ---- @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler::class) -annotation class NullDenied +@AuthorizationDeniedHandler(handlerClass = MaskAnnotationDeniedHandler::class) +annotation class Mask(val value: String) -@NullDenied +@Mask("***") fun myMethod(): String { // ... } ---- ====== +Now you do not have to remember to add both annotations when you need a mask behavior in your method. Make sure to read the <> section for more details on the usage. [[migration-enableglobalmethodsecurity]]