Преглед изворни кода

Support SpEL Returning AuthorizationDecision

Closes gh-14598
Josh Cummings пре 1 година
родитељ
комит
6f07d63938
28 измењених фајлова са 520 додато и 199 уклоњено
  1. 30 1
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationAuthorizationManager.java
  2. 30 1
      config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationReactiveAuthorizationManager.java
  3. 18 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java
  4. 5 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java
  5. 10 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java
  6. 5 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java
  7. 40 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java
  8. 45 1
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java
  9. 5 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java
  10. 5 0
      config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java
  11. 32 1
      core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java
  12. 29 1
      core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java
  13. 29 1
      core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java
  14. 1 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java
  15. 1 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java
  16. 1 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java
  17. 1 1
      core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java
  18. 0 41
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationDecision.java
  19. 12 5
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java
  20. 13 4
      core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java
  21. 0 43
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationDecision.java
  22. 11 5
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java
  23. 12 4
      core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java
  24. 32 3
      core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java
  25. 70 0
      core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java
  26. 33 52
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java
  27. 14 32
      core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java
  28. 36 0
      docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

+ 30 - 1
config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationAuthorizationManager.java

@@ -19,18 +19,30 @@ package org.springframework.security.config.annotation.method.configuration;
 import java.util.function.Supplier;
 
 import io.micrometer.observation.ObservationRegistry;
+import org.aopalliance.intercept.MethodInvocation;
 
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ObservationAuthorizationManager;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
+import org.springframework.security.authorization.method.MethodInvocationResult;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.function.SingletonSupplier;
 
-final class DeferringObservationAuthorizationManager<T> implements AuthorizationManager<T> {
+final class DeferringObservationAuthorizationManager<T>
+		implements AuthorizationManager<T>, MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
 
 	private final Supplier<AuthorizationManager<T>> delegate;
 
+	private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
+
+	private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
+
 	DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
 			AuthorizationManager<T> delegate) {
 		this.delegate = SingletonSupplier.of(() -> {
@@ -40,6 +52,12 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
 			}
 			return new ObservationAuthorizationManager<>(registry, delegate);
 		});
+		if (delegate instanceof MethodAuthorizationDeniedHandler h) {
+			this.handler = h;
+		}
+		if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
+			this.postProcessor = p;
+		}
 	}
 
 	@Override
@@ -47,4 +65,15 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
 		return this.delegate.get().check(authentication, object);
 	}
 
+	@Override
+	public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
+		return this.handler.handle(methodInvocation, authorizationResult);
+	}
+
+	@Override
+	public Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationResult authorizationResult) {
+		return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
+	}
+
 }

+ 30 - 1
config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationReactiveAuthorizationManager.java

@@ -19,19 +19,31 @@ package org.springframework.security.config.annotation.method.configuration;
 import java.util.function.Supplier;
 
 import io.micrometer.observation.ObservationRegistry;
+import org.aopalliance.intercept.MethodInvocation;
 import reactor.core.publisher.Mono;
 
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
+import org.springframework.security.authorization.method.MethodInvocationResult;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.function.SingletonSupplier;
 
-final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
+final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T>,
+		MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
 
 	private final Supplier<ReactiveAuthorizationManager<T>> delegate;
 
+	private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
+
+	private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
+
 	DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
 			ReactiveAuthorizationManager<T> delegate) {
 		this.delegate = SingletonSupplier.of(() -> {
@@ -41,6 +53,12 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
 			}
 			return new ObservationReactiveAuthorizationManager<>(registry, delegate);
 		});
+		if (delegate instanceof MethodAuthorizationDeniedHandler h) {
+			this.handler = h;
+		}
+		if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
+			this.postProcessor = p;
+		}
 	}
 
 	@Override
@@ -48,4 +66,15 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
 		return this.delegate.get().check(authentication, object);
 	}
 
+	@Override
+	public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
+		return this.handler.handle(methodInvocation, authorizationResult);
+	}
+
+	@Override
+	public Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationResult authorizationResult) {
+		return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
+	}
+
 }

+ 18 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java

@@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.method.configuration;
 
 import reactor.core.publisher.Mono;
 
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.core.Authentication;
 import org.springframework.stereotype.Component;
 
@@ -45,4 +47,20 @@ public class Authz {
 		return message != null && message.contains(authentication.getName());
 	}
 
+	public AuthorizationResult checkResult(boolean result) {
+		return new AuthzResult(result);
+	}
+
+	public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
+		return Mono.just(checkResult(result));
+	}
+
+	public static class AuthzResult extends AuthorizationDecision {
+
+		public AuthzResult(boolean granted) {
+			super(granted);
+		}
+
+	}
+
 }

+ 5 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java

@@ -173,6 +173,11 @@ public interface MethodSecurityService {
 	@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
 	UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();
 
+	@PreAuthorize(value = "@authz.checkResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
+	@PostAuthorize(value = "@authz.checkResult(!#result)",
+			postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
+	String checkCustomResult(boolean result);
+
 	class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
 
 		@Override

+ 10 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java

@@ -28,4 +28,14 @@ public class MethodSecurityServiceConfig {
 		return new MethodSecurityServiceImpl();
 	}
 
+	@Bean
+	ReactiveMethodSecurityService reactiveService() {
+		return new ReactiveMethodSecurityServiceImpl();
+	}
+
+	@Bean
+	Authz authz() {
+		return new Authz();
+	}
+
 }

+ 5 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java

@@ -197,4 +197,9 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
 		return new UserRecordWithEmailProtected("username", "useremail@example.com");
 	}
 
+	@Override
+	public String checkCustomResult(boolean result) {
+		return "ok";
+	}
+
 }

+ 40 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java

@@ -66,6 +66,8 @@ import org.springframework.security.authorization.method.AuthorizationAdvisorPro
 import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizeReturnObject;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
 import org.springframework.security.authorization.method.MethodInvocationResult;
 import org.springframework.security.authorization.method.PrePostTemplateDefaults;
 import org.springframework.security.config.Customizer;
@@ -92,6 +94,8 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 /**
  * Tests for {@link PrePostMethodSecurityConfiguration}.
@@ -925,6 +929,23 @@ public class PrePostMethodSecurityConfigurationTests {
 		assertThat(user.name()).isEqualTo("Protected");
 	}
 
+	@Test
+	@WithMockUser
+	void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
+		this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
+		MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
+		MethodAuthorizationDeniedHandler handler = this.spring.getContext()
+			.getBean(MethodAuthorizationDeniedHandler.class);
+		MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
+			.getBean(MethodAuthorizationDeniedPostProcessor.class);
+		assertThat(service.checkCustomResult(false)).isNull();
+		verify(handler).handle(any(), any(Authz.AuthzResult.class));
+		verifyNoInteractions(postProcessor);
+		assertThat(service.checkCustomResult(true)).isNull();
+		verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
+		verifyNoMoreInteractions(handler);
+	}
+
 	private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
 		return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
 	}
@@ -1449,4 +1470,23 @@ public class PrePostMethodSecurityConfigurationTests {
 
 	}
 
+	@EnableMethodSecurity
+	static class CustomResultConfig {
+
+		MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
+
+		MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);
+
+		@Bean
+		MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
+			return this.handler;
+		}
+
+		@Bean
+		MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
+			return this.postProcessor;
+		}
+
+	}
+
 }

+ 45 - 1
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java

@@ -47,6 +47,8 @@ import org.springframework.security.authentication.TestAuthentication;
 import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
 import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
 import org.springframework.security.authorization.method.AuthorizeReturnObject;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.core.GrantedAuthorityDefaults;
 import org.springframework.security.config.test.SpringTestContext;
@@ -54,8 +56,14 @@ import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 import org.springframework.security.core.userdetails.User;
+import org.springframework.security.test.context.support.WithMockUser;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 /**
  * @author Tadaya Tsuyukubo
@@ -65,7 +73,7 @@ public class ReactiveMethodSecurityConfigurationTests {
 
 	public final SpringTestContext spring = new SpringTestContext(this);
 
-	@Autowired
+	@Autowired(required = false)
 	DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;
 
 	@Test
@@ -212,6 +220,23 @@ public class ReactiveMethodSecurityConfigurationTests {
 			.verifyError(AccessDeniedException.class);
 	}
 
+	@Test
+	@WithMockUser
+	void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
+		this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
+		ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
+		MethodAuthorizationDeniedHandler handler = this.spring.getContext()
+			.getBean(MethodAuthorizationDeniedHandler.class);
+		MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
+			.getBean(MethodAuthorizationDeniedPostProcessor.class);
+		assertThat(service.checkCustomResult(false).block()).isNull();
+		verify(handler).handle(any(), any(Authz.AuthzResult.class));
+		verifyNoInteractions(postProcessor);
+		assertThat(service.checkCustomResult(true).block()).isNull();
+		verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
+		verifyNoMoreInteractions(handler);
+	}
+
 	private static Consumer<User.UserBuilder> authorities(String... authorities) {
 		return (builder) -> builder.authorities(authorities);
 	}
@@ -353,4 +378,23 @@ public class ReactiveMethodSecurityConfigurationTests {
 
 	}
 
+	@EnableReactiveMethodSecurity
+	static class CustomResultConfig {
+
+		MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
+
+		MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);
+
+		@Bean
+		MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
+			return this.handler;
+		}
+
+		@Bean
+		MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
+			return this.postProcessor;
+		}
+
+	}
+
 }

+ 5 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java

@@ -85,6 +85,11 @@ public interface ReactiveMethodSecurityService {
 	@Mask(expression = "@myMasker.getMask(returnObject)")
 	Mono<String> postAuthorizeWithMaskAnnotationUsingBean();
 
+	@PreAuthorize(value = "@authz.checkReactiveResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
+	@PostAuthorize(value = "@authz.checkReactiveResult(!#result)",
+			postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
+	Mono<String> checkCustomResult(boolean result);
+
 	class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
 
 		@Override

+ 5 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java

@@ -82,4 +82,9 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurity
 		return Mono.just("ok");
 	}
 
+	@Override
+	public Mono<String> checkCustomResult(boolean result) {
+		return Mono.just("ok");
+	}
+
 }

+ 32 - 1
core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2016 the original author or authors.
+ * 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.
@@ -19,12 +19,43 @@ package org.springframework.security.access.expression;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.EvaluationException;
 import org.springframework.expression.Expression;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationResult;
+import org.springframework.security.authorization.ExpressionAuthorizationDecision;
 
 public final class ExpressionUtils {
 
 	private ExpressionUtils() {
 	}
 
+	/**
+	 * Evaluate a SpEL expression and coerce into an {@link AuthorizationDecision}
+	 * @param expr a SpEL expression
+	 * @param ctx an {@link EvaluationContext}
+	 * @return the resulting {@link AuthorizationDecision}
+	 * @since 6.3
+	 */
+	public static AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) {
+		try {
+			Object result = expr.getValue(ctx);
+			if (result instanceof AuthorizationResult decision) {
+				return decision;
+			}
+			if (result instanceof Boolean granted) {
+				return new ExpressionAuthorizationDecision(granted, expr);
+			}
+			if (result == null) {
+				return null;
+			}
+			throw new IllegalArgumentException(
+					"SpEL expression must return either a Boolean or an AuthorizationDecision");
+		}
+		catch (EvaluationException ex) {
+			throw new IllegalArgumentException("Failed to evaluate expression '" + expr.getExpressionString() + "'",
+					ex);
+		}
+	}
+
 	public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
 		try {
 			return expr.getValue(ctx, Boolean.class);

+ 29 - 1
core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java

@@ -21,11 +21,17 @@ import java.util.function.Supplier;
 import io.micrometer.observation.Observation;
 import io.micrometer.observation.ObservationConvention;
 import io.micrometer.observation.ObservationRegistry;
+import org.aopalliance.intercept.MethodInvocation;
 
 import org.springframework.context.MessageSource;
 import org.springframework.context.MessageSourceAware;
 import org.springframework.context.support.MessageSourceAccessor;
 import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
+import org.springframework.security.authorization.method.MethodInvocationResult;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.SpringSecurityMessageSource;
 import org.springframework.util.Assert;
@@ -36,7 +42,8 @@ import org.springframework.util.Assert;
  * @author Josh Cummings
  * @since 6.0
  */
-public final class ObservationAuthorizationManager<T> implements AuthorizationManager<T>, MessageSourceAware {
+public final class ObservationAuthorizationManager<T> implements AuthorizationManager<T>, MessageSourceAware,
+		MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
 
 	private final ObservationRegistry registry;
 
@@ -46,9 +53,19 @@ public final class ObservationAuthorizationManager<T> implements AuthorizationMa
 
 	private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
 
+	private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
+
+	private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
+
 	public ObservationAuthorizationManager(ObservationRegistry registry, AuthorizationManager<T> delegate) {
 		this.registry = registry;
 		this.delegate = delegate;
+		if (delegate instanceof MethodAuthorizationDeniedHandler h) {
+			this.handler = h;
+		}
+		if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
+			this.postProcessor = p;
+		}
 	}
 
 	@Override
@@ -98,4 +115,15 @@ public final class ObservationAuthorizationManager<T> implements AuthorizationMa
 		this.messages = new MessageSourceAccessor(messageSource);
 	}
 
+	@Override
+	public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
+		return this.handler.handle(methodInvocation, authorizationResult);
+	}
+
+	@Override
+	public Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationResult authorizationResult) {
+		return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
+	}
+
 }

+ 29 - 1
core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java

@@ -20,9 +20,15 @@ import io.micrometer.observation.Observation;
 import io.micrometer.observation.ObservationConvention;
 import io.micrometer.observation.ObservationRegistry;
 import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
+import org.aopalliance.intercept.MethodInvocation;
 import reactor.core.publisher.Mono;
 
 import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
+import org.springframework.security.authorization.method.MethodInvocationResult;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
+import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.Assert;
 
@@ -32,7 +38,8 @@ import org.springframework.util.Assert;
  * @author Josh Cummings
  * @since 6.0
  */
-public final class ObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
+public final class ObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T>,
+		MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
 
 	private final ObservationRegistry registry;
 
@@ -40,10 +47,20 @@ public final class ObservationReactiveAuthorizationManager<T> implements Reactiv
 
 	private ObservationConvention<AuthorizationObservationContext<?>> convention = new AuthorizationObservationConvention();
 
+	private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
+
+	private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
+
 	public ObservationReactiveAuthorizationManager(ObservationRegistry registry,
 			ReactiveAuthorizationManager<T> delegate) {
 		this.registry = registry;
 		this.delegate = delegate;
+		if (delegate instanceof MethodAuthorizationDeniedHandler h) {
+			this.handler = h;
+		}
+		if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
+			this.postProcessor = p;
+		}
 	}
 
 	@Override
@@ -81,4 +98,15 @@ public final class ObservationReactiveAuthorizationManager<T> implements Reactiv
 		this.convention = convention;
 	}
 
+	@Override
+	public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
+		return this.handler.handle(methodInvocation, authorizationResult);
+	}
+
+	@Override
+	public Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationResult authorizationResult) {
+		return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
+	}
+
 }

+ 1 - 1
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java

@@ -184,7 +184,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
 	}
 
 	private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) {
-		if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
+		if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
 			return postProcessableDecision.postProcessResult(mi, decision);
 		}
 		return this.defaultPostProcessor.postProcessResult(mi, decision);

+ 1 - 1
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java

@@ -159,7 +159,7 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
 			return Mono.just(methodInvocationResult.getResult());
 		}
 		return Mono.fromSupplier(() -> {
-			if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
+			if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
 				return postProcessableDecision.postProcessResult(methodInvocationResult, decision);
 			}
 			return this.defaultPostProcessor.postProcessResult(methodInvocationResult, decision);

+ 1 - 1
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java

@@ -257,7 +257,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
 	}
 
 	private Object handle(MethodInvocation mi, AuthorizationDecision decision) {
-		if (decision instanceof MethodAuthorizationDeniedHandler handler) {
+		if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
 			return handler.handle(mi, decision);
 		}
 		return this.defaultHandler.handle(mi, decision);

+ 1 - 1
core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java

@@ -162,7 +162,7 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
 
 	private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
 		return Mono.fromSupplier(() -> {
-			if (decision instanceof MethodAuthorizationDeniedHandler handler) {
+			if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
 				return handler.handle(mi, decision);
 			}
 			return this.defaultHandler.handle(mi, decision);

+ 0 - 41
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationDecision.java

@@ -1,41 +0,0 @@
-/*
- * 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 org.springframework.expression.Expression;
-import org.springframework.security.authorization.AuthorizationResult;
-import org.springframework.security.authorization.ExpressionAuthorizationDecision;
-import org.springframework.util.Assert;
-
-class PostAuthorizeAuthorizationDecision extends ExpressionAuthorizationDecision
-		implements MethodAuthorizationDeniedPostProcessor {
-
-	private final MethodAuthorizationDeniedPostProcessor postProcessor;
-
-	PostAuthorizeAuthorizationDecision(boolean granted, Expression expression,
-			MethodAuthorizationDeniedPostProcessor postProcessor) {
-		super(granted, expression);
-		Assert.notNull(postProcessor, "postProcessor cannot be null");
-		this.postProcessor = postProcessor;
-	}
-
-	@Override
-	public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult result) {
-		return this.postProcessor.postProcessResult(methodInvocationResult, result);
-	}
-
-}

+ 12 - 5
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java

@@ -27,6 +27,7 @@ import org.springframework.security.access.expression.method.MethodSecurityExpre
 import org.springframework.security.access.prepost.PostAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.core.Authentication;
 
 /**
@@ -37,7 +38,8 @@ import org.springframework.security.core.Authentication;
  * @author Evgeniy Cheban
  * @since 5.6
  */
-public final class PostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
+public final class PostAuthorizeAuthorizationManager
+		implements AuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
 
 	private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
 
@@ -88,13 +90,18 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
 		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
 			return null;
 		}
-		PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
 		MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
 		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
 		expressionHandler.setReturnObject(mi.getResult(), ctx);
-		boolean granted = ExpressionUtils.evaluateAsBoolean(postAuthorizeAttribute.getExpression(), ctx);
-		return new PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(),
-				postAuthorizeAttribute.getPostProcessor());
+		return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
+	}
+
+	@Override
+	public Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationResult authorizationResult) {
+		ExpressionAttribute attribute = this.registry.getAttribute(methodInvocationResult.getMethodInvocation());
+		PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
+		return postAuthorizeAttribute.getPostProcessor().postProcessResult(methodInvocationResult, authorizationResult);
 	}
 
 }

+ 13 - 4
core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java

@@ -24,6 +24,7 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PostAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.Assert;
@@ -37,7 +38,7 @@ import org.springframework.util.Assert;
  * @since 5.8
  */
 public final class PostAuthorizeReactiveAuthorizationManager
-		implements ReactiveAuthorizationManager<MethodInvocationResult> {
+		implements ReactiveAuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
 
 	private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
 
@@ -82,15 +83,23 @@ public final class PostAuthorizeReactiveAuthorizationManager
 		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
 			return Mono.empty();
 		}
-		PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
+
 		MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
 		// @formatter:off
 		return authentication
 				.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
 				.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
-				.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
-				.map((granted) -> new PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(), postAuthorizeAttribute.getPostProcessor()));
+				.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
+				.cast(AuthorizationDecision.class);
 		// @formatter:on
 	}
 
+	@Override
+	public Object postProcessResult(MethodInvocationResult methodInvocationResult,
+			AuthorizationResult authorizationResult) {
+		ExpressionAttribute attribute = this.registry.getAttribute(methodInvocationResult.getMethodInvocation());
+		PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
+		return postAuthorizeAttribute.getPostProcessor().postProcessResult(methodInvocationResult, authorizationResult);
+	}
+
 }

+ 0 - 43
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationDecision.java

@@ -1,43 +0,0 @@
-/*
- * 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 org.aopalliance.intercept.MethodInvocation;
-
-import org.springframework.expression.Expression;
-import org.springframework.security.authorization.AuthorizationResult;
-import org.springframework.security.authorization.ExpressionAuthorizationDecision;
-import org.springframework.util.Assert;
-
-class PreAuthorizeAuthorizationDecision extends ExpressionAuthorizationDecision
-		implements MethodAuthorizationDeniedHandler {
-
-	private final MethodAuthorizationDeniedHandler handler;
-
-	PreAuthorizeAuthorizationDecision(boolean granted, Expression expression,
-			MethodAuthorizationDeniedHandler handler) {
-		super(granted, expression);
-		Assert.notNull(handler, "handler cannot be null");
-		this.handler = handler;
-	}
-
-	@Override
-	public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
-		return this.handler.handle(methodInvocation, result);
-	}
-
-}

+ 11 - 5
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java

@@ -27,6 +27,7 @@ import org.springframework.security.access.expression.method.MethodSecurityExpre
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.core.Authentication;
 
 /**
@@ -37,7 +38,8 @@ import org.springframework.security.core.Authentication;
  * @author Evgeniy Cheban
  * @since 5.6
  */
-public final class PreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+public final class PreAuthorizeAuthorizationManager
+		implements AuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
 
 	private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
 
@@ -80,11 +82,15 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
 		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
 			return null;
 		}
-		PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
 		EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
-		boolean granted = ExpressionUtils.evaluateAsBoolean(preAuthorizeAttribute.getExpression(), ctx);
-		return new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(),
-				preAuthorizeAttribute.getHandler());
+		return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
+	}
+
+	@Override
+	public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
+		ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation);
+		PreAuthorizeExpressionAttribute postAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
+		return postAuthorizeAttribute.getHandler().handle(methodInvocation, authorizationResult);
 	}
 
 }

+ 12 - 4
core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java

@@ -24,6 +24,7 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.util.Assert;
@@ -36,7 +37,8 @@ import org.springframework.util.Assert;
  * @author Evgeniy Cheban
  * @since 5.8
  */
-public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
+public final class PreAuthorizeReactiveAuthorizationManager
+		implements ReactiveAuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
 
 	private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
 
@@ -79,13 +81,19 @@ public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveA
 		if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
 			return Mono.empty();
 		}
-		PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
 		// @formatter:off
 		return authentication
 				.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
-				.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
-				.map((granted) -> new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(), preAuthorizeAttribute.getHandler()));
+				.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
+				.cast(AuthorizationDecision.class);
 		// @formatter:on
 	}
 
+	@Override
+	public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
+		ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation);
+		PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
+		return preAuthorizeAttribute.getHandler().handle(methodInvocation, authorizationResult);
+	}
+
 }

+ 32 - 3
core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java

@@ -21,6 +21,8 @@ import reactor.core.publisher.Mono;
 import org.springframework.expression.EvaluationContext;
 import org.springframework.expression.EvaluationException;
 import org.springframework.expression.Expression;
+import org.springframework.security.authorization.AuthorizationResult;
+import org.springframework.security.authorization.ExpressionAuthorizationDecision;
 
 /**
  * For internal use only, as this contract is likely to change.
@@ -30,6 +32,33 @@ import org.springframework.expression.Expression;
  */
 final class ReactiveExpressionUtils {
 
+	static Mono<AuthorizationResult> evaluate(Expression expr, EvaluationContext ctx) {
+		return Mono.defer(() -> {
+			Object value;
+			try {
+				value = expr.getValue(ctx);
+			}
+			catch (EvaluationException ex) {
+				return Mono.error(() -> new IllegalArgumentException(
+						"Failed to evaluate expression '" + expr.getExpressionString() + "'", ex));
+			}
+			if (value instanceof Mono<?> mono) {
+				return mono.flatMap((data) -> adapt(expr, data));
+			}
+			return adapt(expr, value);
+		});
+	}
+
+	private static Mono<AuthorizationResult> adapt(Expression expr, Object value) {
+		if (value instanceof Boolean granted) {
+			return Mono.just(new ExpressionAuthorizationDecision(granted, expr));
+		}
+		if (value instanceof AuthorizationResult decision) {
+			return Mono.just(decision);
+		}
+		return createInvalidReturnTypeMono(expr);
+	}
+
 	static Mono<Boolean> evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
 		return Mono.defer(() -> {
 			Object value;
@@ -56,9 +85,9 @@ final class ReactiveExpressionUtils {
 		});
 	}
 
-	private static Mono<Boolean> createInvalidReturnTypeMono(Expression expr) {
-		return Mono.error(() -> new IllegalStateException(
-				"Expression: '" + expr.getExpressionString() + "' must return boolean or Mono<Boolean>"));
+	private static <T> Mono<T> createInvalidReturnTypeMono(Expression expr) {
+		return Mono.error(() -> new IllegalStateException("Expression: '" + expr.getExpressionString()
+				+ "' must return boolean, Mono<Boolean>, AuthorizationResult, or Mono<AuthorizationResult>"));
 	}
 
 	private ReactiveExpressionUtils() {

+ 70 - 0
core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java

@@ -0,0 +1,70 @@
+/*
+ * 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.access.expression;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.ExpressionAuthorizationDecision;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExpressionUtilsTests {
+
+	private final Object details = new Object();
+
+	@Test
+	public void evaluateWhenAuthorizationDecisionThenReturns() {
+		SpelExpressionParser parser = new SpelExpressionParser();
+		Expression expression = parser.parseExpression("#root.returnDecision()");
+		StandardEvaluationContext context = new StandardEvaluationContext(this);
+		assertThat(ExpressionUtils.evaluate(expression, context)).isInstanceOf(AuthorizationDecisionDetails.class)
+			.extracting("details")
+			.isEqualTo(this.details);
+	}
+
+	@Test
+	public void evaluateWhenBooleanThenReturnsExpressionAuthorizationDecision() {
+		SpelExpressionParser parser = new SpelExpressionParser();
+		Expression expression = parser.parseExpression("#root.returnResult()");
+		StandardEvaluationContext context = new StandardEvaluationContext(this);
+		assertThat(ExpressionUtils.evaluate(expression, context)).isInstanceOf(ExpressionAuthorizationDecision.class);
+	}
+
+	public AuthorizationDecision returnDecision() {
+		return new AuthorizationDecisionDetails(false, this.details);
+	}
+
+	public boolean returnResult() {
+		return false;
+	}
+
+	static final class AuthorizationDecisionDetails extends AuthorizationDecision {
+
+		final Object details;
+
+		AuthorizationDecisionDetails(boolean granted, Object details) {
+			super(granted);
+			this.details = details;
+		}
+
+	}
+
+}

+ 33 - 52
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java

@@ -19,16 +19,15 @@ package org.springframework.security.authorization.method;
 import org.aopalliance.intercept.MethodInvocation;
 import org.assertj.core.api.InstanceOfAssertFactories;
 import org.junit.jupiter.api.Test;
+import org.mockito.invocation.InvocationOnMock;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.expression.common.LiteralExpression;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.intercept.method.MockMethodInvocation;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationDeniedException;
-import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -125,10 +124,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
 		given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
-		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		given(mockReactiveAuthorizationManager.check(any(), any()))
-			.will((invocation) -> Mono.just(createDecision(new MaskingPostProcessor())));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
+		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -144,15 +143,16 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
 		given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
-		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		given(mockReactiveAuthorizationManager.check(any(), any())).willAnswer((invocation) -> {
-			MethodInvocationResult argument = invocation.getArgument(1);
-			if ("john".equals(argument.getResult())) {
-				return Mono.just(new AuthorizationDecision(true));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer((invocation) -> {
+			MethodInvocationResult argument = invocation.getArgument(0);
+			if (!"john".equals(argument.getResult())) {
+				return monoMasking(invocation);
 			}
-			return Mono.just(createDecision(new MaskingPostProcessor()));
+			return Mono.just(argument.getResult());
 		});
+		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -168,11 +168,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
-		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
-				new LiteralExpression("1234"), new MaskingPostProcessor());
-		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
+		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -187,11 +186,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
-		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
-				new LiteralExpression("1234"), new MonoMaskingPostProcessor());
-		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::monoMasking);
+		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -206,11 +204,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
-		ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
-				new LiteralExpression("1234"), new NullPostProcessor());
-		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willReturn(null);
+		given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
 		AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -238,34 +235,18 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
 		verify(mockReactiveAuthorizationManager).check(any(), any());
 	}
 
-	private PostAuthorizeAuthorizationDecision createDecision(MethodAuthorizationDeniedPostProcessor postProcessor) {
-		return new PostAuthorizeAuthorizationDecision(false, new LiteralExpression("1234"), postProcessor);
+	private Object masking(InvocationOnMock invocation) {
+		MethodInvocationResult result = invocation.getArgument(0);
+		return result.getResult() + "-masked";
 	}
 
-	static class MaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
-
-		@Override
-		public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
-			return contextObject.getResult() + "-masked";
-		}
-
+	private Object monoMasking(InvocationOnMock invocation) {
+		MethodInvocationResult result = invocation.getArgument(0);
+		return Mono.just(result.getResult() + "-masked");
 	}
 
-	static class MonoMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
-
-		@Override
-		public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
-			return Mono.just(contextObject.getResult() + "-masked");
-		}
-
-	}
-
-	static class NullPostProcessor implements MethodAuthorizationDeniedPostProcessor {
-
-		@Override
-		public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
-			return null;
-		}
+	interface HandlingReactiveAuthorizationManager
+			extends ReactiveAuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
 
 	}
 

+ 14 - 32
core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java

@@ -23,12 +23,10 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.expression.common.LiteralExpression;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.intercept.method.MockMethodInvocation;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationDeniedException;
-import org.springframework.security.authorization.AuthorizationResult;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -125,11 +123,10 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
-		ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
-				new LiteralExpression("1234"), new MaskingPostProcessor());
-		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
+		given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn("***");
 		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -144,11 +141,10 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
 		given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
-		ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
-				new LiteralExpression("1234"), new MonoMaskingPostProcessor());
-		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
+		given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***"));
 		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -163,11 +159,10 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		MethodInvocation mockMethodInvocation = spy(
 				new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
 		given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
-		ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
-				ReactiveAuthorizationManager.class);
-		PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
-				new LiteralExpression("1234"), new MonoMaskingPostProcessor());
-		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
+		HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
+				HandlingReactiveAuthorizationManager.class);
+		given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
+		given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***"));
 		AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
 				Pointcut.TRUE, mockReactiveAuthorizationManager);
 		Object result = interceptor.invoke(mockMethodInvocation);
@@ -214,21 +209,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
 		verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
 	}
 
-	static class MaskingPostProcessor implements MethodAuthorizationDeniedHandler {
-
-		@Override
-		public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
-			return "***";
-		}
-
-	}
-
-	static class MonoMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
-
-		@Override
-		public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
-			return Mono.just("***");
-		}
+	interface HandlingReactiveAuthorizationManager
+			extends ReactiveAuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
 
 	}
 

+ 36 - 0
docs/modules/ROOT/pages/servlet/authorization/method-security.adoc

@@ -1215,6 +1215,42 @@ Spring Security will invoke the given method on that bean for each method invoca
 What's nice about this is all your authorization logic is in a separate class that can be independently unit tested and verified for correctness.
 It also has access to the full Java language.
 
+[TIP]
+In addition to returning a `Boolean`, you can also return `null` to indicate that the code abstains from making a decision.
+
+If you want to include more information about the nature of the decision, you can instead return a custom `AuthorizationDecision` like this:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Component("authz")
+public class AuthorizationLogic {
+    public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) {
+        // ... authorization logic
+        return new MyAuthorizationDecision(false, details);
+    }
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Component("authz")
+open class AuthorizationLogic {
+    fun decide(val operations: MethodSecurityExpressionOperations): AuthorizationDecision {
+        // ... authorization logic
+        return MyAuthorizationDecision(false, details)
+    }
+}
+----
+======
+
+Then, you can access the custom details when you <<fallback-values-authorization-denied, customize how the authorization result is handled>>.
+
 [[custom-authorization-managers]]
 === Using a Custom Authorization Manager