Skip to content

Consider handler bean reference for HandleAuthorizationDenied #16970

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -30,6 +30,7 @@
* thrown during method invocation
*
* @author Marcus da Coregio
* @author Evgeniy Cheban
* @since 6.3
* @see AuthorizationManagerAfterMethodInterceptor
* @see AuthorizationManagerBeforeMethodInterceptor
Expand All @@ -47,4 +48,13 @@
*/
Class<? extends MethodAuthorizationDeniedHandler> 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 "";

}
Original file line number Diff line number Diff line change
@@ -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<HandleAuthorizationDenied> handleAuthorizationDeniedScanner = SecurityAnnotationScanners
.requireUnique(HandleAuthorizationDenied.class);

private BiFunction<String, Class<? extends MethodAuthorizationDeniedHandler>, 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<? extends MethodAuthorizationDeniedHandler> 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);
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand All @@ -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.
Expand All @@ -39,21 +36,12 @@
*/
final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {

private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();

private final SecurityAnnotationScanner<HandleAuthorizationDenied> handleAuthorizationDeniedScanner = SecurityAnnotationScanners
.requireUnique(HandleAuthorizationDenied.class);

private Function<Class<? extends MethodAuthorizationDeniedHandler>, MethodAuthorizationDeniedHandler> handlerResolver;
private final MethodAuthorizationDeniedHandlerResolver handlerResolver = new MethodAuthorizationDeniedHandlerResolver(
PostAuthorizeAuthorizationManager.class);

private SecurityAnnotationScanner<PostAuthorize> postAuthorizeScanner = SecurityAnnotationScanners
.requireUnique(PostAuthorize.class);

PostAuthorizeExpressionAttributeRegistry() {
this.handlerResolver = (clazz) -> new ReflectiveMethodAuthorizationDeniedHandler(clazz,
PostAuthorizeAuthorizationManager.class);
}

@NonNull
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Expand All @@ -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);
Expand All @@ -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<? extends MethodAuthorizationDeniedHandler> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -38,21 +35,12 @@
*/
final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {

private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();

private final SecurityAnnotationScanner<HandleAuthorizationDenied> handleAuthorizationDeniedScanner = SecurityAnnotationScanners
.requireUnique(HandleAuthorizationDenied.class);

private Function<Class<? extends MethodAuthorizationDeniedHandler>, MethodAuthorizationDeniedHandler> handlerResolver;
private final MethodAuthorizationDeniedHandlerResolver handlerResolver = new MethodAuthorizationDeniedHandlerResolver(
PreAuthorizeAuthorizationManager.class);

private SecurityAnnotationScanner<PreAuthorize> preAuthorizeScanner = SecurityAnnotationScanners
.requireUnique(PreAuthorize.class);

PreAuthorizeExpressionAttributeRegistry() {
this.handlerResolver = (clazz) -> new ReflectiveMethodAuthorizationDeniedHandler(clazz,
PreAuthorizeAuthorizationManager.class);
}

@NonNull
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Expand All @@ -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);
Expand All @@ -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<? extends MethodAuthorizationDeniedHandler> 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);
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {
Expand Down
Loading