diff --git a/core/src/main/java/org/springframework/security/authorization/method/HandleAuthorizationDenied.java b/core/src/main/java/org/springframework/security/authorization/method/HandleAuthorizationDenied.java index d191e3dae1..fb8fe7bb64 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/HandleAuthorizationDenied.java +++ b/core/src/main/java/org/springframework/security/authorization/method/HandleAuthorizationDenied.java @@ -30,6 +30,7 @@ import java.lang.annotation.Target; * thrown during method invocation * * @author Marcus da Coregio + * @author Evgeniy Cheban * @since 6.3 * @see AuthorizationManagerAfterMethodInterceptor * @see AuthorizationManagerBeforeMethodInterceptor @@ -47,4 +48,13 @@ public @interface HandleAuthorizationDenied { */ Class handlerClass() default ThrowingMethodAuthorizationDeniedHandler.class; + /** + * Specifies a {@link MethodAuthorizationDeniedHandler} bean name to be used to handle + * denied method invocation. + * @return the {@link MethodAuthorizationDeniedHandler} bean name to be used to handle + * denied method invocation + * @since 7.1 + */ + String handler() default ""; + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandlerResolver.java b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandlerResolver.java new file mode 100644 index 0000000000..37fc0536b4 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandlerResolver.java @@ -0,0 +1,82 @@ +/* + * Copyright 2004-present 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.reflect.Method; +import java.util.Arrays; +import java.util.function.BiFunction; + +import org.springframework.context.ApplicationContext; +import org.springframework.security.core.annotation.SecurityAnnotationScanner; +import org.springframework.security.core.annotation.SecurityAnnotationScanners; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + */ +final class MethodAuthorizationDeniedHandlerResolver { + + private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler(); + + private final SecurityAnnotationScanner handleAuthorizationDeniedScanner = SecurityAnnotationScanners + .requireUnique(HandleAuthorizationDenied.class); + + private BiFunction, MethodAuthorizationDeniedHandler> resolver; + + MethodAuthorizationDeniedHandlerResolver(Class managerClass) { + this.resolver = (beanName, handlerClass) -> new ReflectiveMethodAuthorizationDeniedHandler(handlerClass, + managerClass); + } + + void setContext(ApplicationContext context) { + Assert.notNull(context, "context cannot be null"); + this.resolver = (beanName, handlerClass) -> doResolve(context, beanName, handlerClass); + } + + MethodAuthorizationDeniedHandler resolve(Method method, Class targetClass) { + HandleAuthorizationDenied deniedHandler = this.handleAuthorizationDeniedScanner.scan(method, targetClass); + if (deniedHandler != null) { + return this.resolver.apply(deniedHandler.handler(), deniedHandler.handlerClass()); + } + return this.defaultHandler; + } + + private MethodAuthorizationDeniedHandler doResolve(ApplicationContext context, String beanName, + Class handlerClass) { + if (StringUtils.hasText(beanName)) { + return context.getBean(beanName, MethodAuthorizationDeniedHandler.class); + } + if (handlerClass == this.defaultHandler.getClass()) { + return this.defaultHandler; + } + String[] beanNames = context.getBeanNamesForType(handlerClass); + if (beanNames.length == 0) { + throw new IllegalStateException("Could not find a bean of type " + handlerClass.getName()); + } + if (beanNames.length > 1) { + throw new IllegalStateException(""" + Expected to find a single bean of type %s but found %s + consider using 'handler' attribute to refer to specific bean + """.formatted(handlerClass.getName(), Arrays.toString(beanNames))); + } + return context.getBean(beanNames[0], handlerClass); + } + +} 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 9803158c03..f123ec9b10 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 @@ -17,8 +17,6 @@ package org.springframework.security.authorization.method; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -28,7 +26,6 @@ import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.SecurityAnnotationScanner; import org.springframework.security.core.annotation.SecurityAnnotationScanners; -import org.springframework.util.Assert; /** * For internal use only, as this contract is likely to change. @@ -39,21 +36,12 @@ import org.springframework.util.Assert; */ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { - private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler(); - - private final SecurityAnnotationScanner handleAuthorizationDeniedScanner = SecurityAnnotationScanners - .requireUnique(HandleAuthorizationDenied.class); - - private Function, MethodAuthorizationDeniedHandler> handlerResolver; + private final MethodAuthorizationDeniedHandlerResolver handlerResolver = new MethodAuthorizationDeniedHandlerResolver( + PostAuthorizeAuthorizationManager.class); private SecurityAnnotationScanner postAuthorizeScanner = SecurityAnnotationScanners .requireUnique(PostAuthorize.class); - PostAuthorizeExpressionAttributeRegistry() { - this.handlerResolver = (clazz) -> new ReflectiveMethodAuthorizationDeniedHandler(clazz, - PostAuthorizeAuthorizationManager.class); - } - @Override @Nullable ExpressionAttribute resolveAttribute(Method method, @Nullable Class targetClass) { PostAuthorize postAuthorize = findPostAuthorizeAnnotation(method, targetClass); @@ -61,19 +49,11 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA return null; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(postAuthorize.value()); - MethodAuthorizationDeniedHandler deniedHandler = resolveHandler(method, targetClass); + MethodAuthorizationDeniedHandler deniedHandler = this.handlerResolver.resolve(method, + targetClass(method, targetClass)); return new PostAuthorizeExpressionAttribute(expression, deniedHandler); } - private MethodAuthorizationDeniedHandler resolveHandler(Method method, @Nullable Class targetClass) { - Class targetClassToUse = targetClass(method, targetClass); - HandleAuthorizationDenied deniedHandler = this.handleAuthorizationDeniedScanner.scan(method, targetClassToUse); - if (deniedHandler != null) { - return this.handlerResolver.apply(deniedHandler.handlerClass()); - } - return this.defaultHandler; - } - private @Nullable PostAuthorize findPostAuthorizeAnnotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.postAuthorizeScanner.scan(method, targetClassToUse); @@ -85,28 +65,11 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA * @param context the {@link ApplicationContext} to use */ void setApplicationContext(ApplicationContext context) { - Assert.notNull(context, "context cannot be null"); - this.handlerResolver = (clazz) -> resolveHandler(context, clazz); + this.handlerResolver.setContext(context); } void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { this.postAuthorizeScanner = SecurityAnnotationScanners.requireUnique(PostAuthorize.class, templateDefaults); } - private MethodAuthorizationDeniedHandler resolveHandler(ApplicationContext context, - Class handlerClass) { - if (handlerClass == this.defaultHandler.getClass()) { - return this.defaultHandler; - } - String[] beanNames = context.getBeanNamesForType(handlerClass); - if (beanNames.length == 0) { - throw new IllegalStateException("Could not find a bean of type " + handlerClass.getName()); - } - if (beanNames.length > 1) { - throw new IllegalStateException("Expected to find a single bean of type " + handlerClass.getName() - + " but found " + Arrays.toString(beanNames)); - } - return context.getBean(beanNames[0], handlerClass); - } - } 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 708231df3e..ba5d9e885e 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 @@ -17,8 +17,6 @@ package org.springframework.security.authorization.method; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -28,7 +26,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.SecurityAnnotationScanner; import org.springframework.security.core.annotation.SecurityAnnotationScanners; -import org.springframework.util.Assert; /** * For internal use only, as this contract is likely to change. @@ -39,21 +36,12 @@ import org.springframework.util.Assert; */ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { - private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler(); - - private final SecurityAnnotationScanner handleAuthorizationDeniedScanner = SecurityAnnotationScanners - .requireUnique(HandleAuthorizationDenied.class); - - private Function, MethodAuthorizationDeniedHandler> handlerResolver; + private final MethodAuthorizationDeniedHandlerResolver handlerResolver = new MethodAuthorizationDeniedHandlerResolver( + PreAuthorizeAuthorizationManager.class); private SecurityAnnotationScanner preAuthorizeScanner = SecurityAnnotationScanners .requireUnique(PreAuthorize.class); - PreAuthorizeExpressionAttributeRegistry() { - this.handlerResolver = (clazz) -> new ReflectiveMethodAuthorizationDeniedHandler(clazz, - PreAuthorizeAuthorizationManager.class); - } - @Override @Nullable ExpressionAttribute resolveAttribute(Method method, @Nullable Class targetClass) { PreAuthorize preAuthorize = findPreAuthorizeAnnotation(method, targetClass); @@ -61,19 +49,11 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt return null; } Expression expression = getExpressionHandler().getExpressionParser().parseExpression(preAuthorize.value()); - MethodAuthorizationDeniedHandler handler = resolveHandler(method, targetClass); + MethodAuthorizationDeniedHandler handler = this.handlerResolver.resolve(method, + targetClass(method, targetClass)); return new PreAuthorizeExpressionAttribute(expression, handler); } - private MethodAuthorizationDeniedHandler resolveHandler(Method method, @Nullable Class targetClass) { - Class targetClassToUse = targetClass(method, targetClass); - HandleAuthorizationDenied deniedHandler = this.handleAuthorizationDeniedScanner.scan(method, targetClassToUse); - if (deniedHandler != null) { - return this.handlerResolver.apply(deniedHandler.handlerClass()); - } - return this.defaultHandler; - } - private @Nullable PreAuthorize findPreAuthorizeAnnotation(Method method, @Nullable Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.preAuthorizeScanner.scan(method, targetClassToUse); @@ -85,28 +65,11 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt * @param context the {@link ApplicationContext} to use */ void setApplicationContext(ApplicationContext context) { - Assert.notNull(context, "context cannot be null"); - this.handlerResolver = (clazz) -> resolveHandler(context, clazz); + this.handlerResolver.setContext(context); } void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { this.preAuthorizeScanner = SecurityAnnotationScanners.requireUnique(PreAuthorize.class, defaults); } - private MethodAuthorizationDeniedHandler resolveHandler(ApplicationContext context, - Class handlerClass) { - if (handlerClass == this.defaultHandler.getClass()) { - return this.defaultHandler; - } - String[] beanNames = context.getBeanNamesForType(handlerClass); - if (beanNames.length == 0) { - throw new IllegalStateException("Could not find a bean of type " + handlerClass.getName()); - } - if (beanNames.length > 1) { - throw new IllegalStateException("Expected to find a single bean of type " + handlerClass.getName() - + " but found " + Arrays.toString(beanNames)); - } - return context.getBean(beanNames[0], handlerClass); - } - } diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java index 9e418096f9..ead326085b 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java @@ -26,6 +26,7 @@ import java.util.function.Supplier; import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; @@ -177,6 +178,27 @@ public class PostAuthorizeAuthorizationManagerTests { .isThrownBy(() -> handleDeniedInvocationResult("methodOne", manager)); } + @Test + public void checkWhenHandlerDeniedApplicationContextHandlerSpecifiedThenLooksForBean() throws Exception { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("deniedHandler", NoDefaultConstructorHandler.class, + () -> new NoDefaultConstructorHandler(new Object())); + context.refresh(); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + manager.setApplicationContext(context); + assertThat(handleDeniedInvocationResult("methodThree", manager)).isNull(); + } + + @Test + public void checkWhenHandlerDeniedApplicationContextHandlerSpecifiedThenLooksForBeanNotFound() { + GenericApplicationContext context = new GenericApplicationContext(); + context.refresh(); + PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); + manager.setApplicationContext(context); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> handleDeniedInvocationResult("methodThree", manager)); + } + private Object handleDeniedInvocationResult(String methodName, PostAuthorizeAuthorizationManager manager) throws Exception { MethodInvocation invocation = new MockMethodInvocation(new UsingHandleDeniedAuthorization(), @@ -277,6 +299,12 @@ public class PostAuthorizeAuthorizationManagerTests { return "ok"; } + @HandleAuthorizationDenied(handler = "deniedHandler") + @PostAuthorize("denyAll()") + public String methodThree() { + return "ok"; + } + } public static final class NullHandler implements MethodAuthorizationDeniedHandler { diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java index b9439ca3d6..8ad996458d 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java @@ -24,6 +24,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; import org.springframework.aop.TargetClassAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; @@ -158,6 +159,27 @@ public class PreAuthorizeAuthorizationManagerTests { .isThrownBy(() -> handleDeniedInvocationResult("methodOne", manager)); } + @Test + public void checkWhenHandlerDeniedApplicationContextHandlerSpecifiedThenLooksForBean() throws Exception { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("deniedHandler", NoDefaultConstructorHandler.class, + () -> new NoDefaultConstructorHandler(new Object())); + context.refresh(); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + manager.setApplicationContext(context); + assertThat(handleDeniedInvocationResult("methodThree", manager)).isNull(); + } + + @Test + public void checkWhenHandlerDeniedApplicationContextHandlerSpecifiedThenLooksForBeanNotFound() { + GenericApplicationContext context = new GenericApplicationContext(); + context.refresh(); + PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); + manager.setApplicationContext(context); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> handleDeniedInvocationResult("methodThree", manager)); + } + private Object handleDeniedInvocationResult(String methodName, PreAuthorizeAuthorizationManager manager) throws Exception { MethodInvocation invocation = new MockMethodInvocation(new UsingHandleDeniedAuthorization(), @@ -281,6 +303,12 @@ public class PreAuthorizeAuthorizationManagerTests { return "ok"; } + @HandleAuthorizationDenied(handler = "deniedHandler") + @PreAuthorize("denyAll()") + public String methodThree() { + return "ok"; + } + } public static final class NullHandler implements MethodAuthorizationDeniedHandler {