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 7a28e9324ef..949e8f7fccd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -30,6 +30,7 @@ * thrown during method invocation * * @author Marcus da Coregio + * @author Evgeniy Cheban * @since 6.3 * @see AuthorizationManagerAfterMethodInterceptor * @see AuthorizationManagerBeforeMethodInterceptor @@ -47,4 +48,13 @@ */ 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 6.5 + */ + 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 00000000000..8d940c14f46 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandlerResolver.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2025 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 " + handlerClass.getName() + + " but found " + Arrays.toString(beanNames) + + " consider using 'handler' attribute to refer to specific bean"); + } + 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 7dc96c106f7..cb5803ec78a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -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 reactor.util.annotation.NonNull; @@ -28,7 +26,6 @@ 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 @@ */ 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); - } - @NonNull @Override ExpressionAttribute resolveAttribute(Method method, Class targetClass) { @@ -62,19 +50,11 @@ ExpressionAttribute resolveAttribute(Method method, Class targetClass) { return ExpressionAttribute.NULL_ATTRIBUTE; } 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, 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 PostAuthorize findPostAuthorizeAnnotation(Method method, Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.postAuthorizeScanner.scan(method, targetClassToUse); @@ -86,28 +66,11 @@ private PostAuthorize findPostAuthorizeAnnotation(Method method, Class target * @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 b2700cef349..93c7dbd597f 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.springframework.context.ApplicationContext; import org.springframework.expression.Expression; @@ -27,7 +25,6 @@ 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. @@ -38,21 +35,12 @@ */ 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); - } - @NonNull @Override ExpressionAttribute resolveAttribute(Method method, Class targetClass) { @@ -61,19 +49,11 @@ ExpressionAttribute resolveAttribute(Method method, Class targetClass) { return ExpressionAttribute.NULL_ATTRIBUTE; } 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, 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 PreAuthorize findPreAuthorizeAnnotation(Method method, Class targetClass) { Class targetClassToUse = targetClass(method, targetClass); return this.preAuthorizeScanner.scan(method, targetClassToUse); @@ -85,28 +65,11 @@ private PreAuthorize findPreAuthorizeAnnotation(Method method, Class targetCl * @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 4a52ff7b5ed..f9279052359 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -26,6 +26,7 @@ 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; @@ -179,6 +180,27 @@ public void checkWhenHandlerDeniedApplicationContextThenLooksForBean() throws Ex .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(), @@ -279,6 +301,12 @@ public String methodTwo() { 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 9795dec9746..bcf527f04f8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -24,6 +24,7 @@ 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; @@ -160,6 +161,27 @@ public void checkWhenHandlerDeniedApplicationContextThenLooksForBean() throws Ex .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(), @@ -283,6 +305,12 @@ public String methodTwo() { return "ok"; } + @HandleAuthorizationDenied(handler = "deniedHandler") + @PreAuthorize("denyAll()") + public String methodThree() { + return "ok"; + } + } public static final class NullHandler implements MethodAuthorizationDeniedHandler {