Jelajahi Sumber

Add AuthorizationProxyFactory Reactive Support

Issue gh-14596
Josh Cummings 1 tahun lalu
induk
melakukan
c611b7e33b

+ 74 - 15
config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java

@@ -16,9 +16,18 @@
 
 package org.springframework.security.config.annotation.method.configuration;
 
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
 import io.micrometer.observation.ObservationRegistry;
+import org.aopalliance.aop.Advice;
+import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.config.BeanDefinition;
@@ -29,6 +38,7 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri
 import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
 import org.springframework.security.authorization.ReactiveAuthorizationManager;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
 import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.MethodInvocationResult;
@@ -38,6 +48,7 @@ import org.springframework.security.authorization.method.PreAuthorizeReactiveAut
 import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
 import org.springframework.security.authorization.method.PrePostTemplateDefaults;
 import org.springframework.security.config.core.GrantedAuthorityDefaults;
+import org.springframework.util.function.SingletonSupplier;
 
 /**
  * Configuration for a {@link ReactiveAuthenticationManager} based Method Security.
@@ -46,54 +57,56 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults;
  * @since 5.8
  */
 @Configuration(proxyBeanMethods = false)
-final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
+final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements AopInfrastructureBean {
 
 	@Bean
 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-	static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
-			MethodSecurityExpressionHandler expressionHandler,
+	static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
 			ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
 		PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(
 				expressionHandler);
-		defaultsObjectProvider.ifAvailable(interceptor::setTemplateDefaults);
-		return interceptor;
+		return new DeferringMethodInterceptor<>(interceptor,
+				(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
 	}
 
 	@Bean
 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-	static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
+	static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
 			MethodSecurityExpressionHandler expressionHandler,
 			ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
 			ObjectProvider<ObservationRegistry> registryProvider) {
 		PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
 				expressionHandler);
-		defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults);
 		ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
-		return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
+		AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
+			.preAuthorize(authorizationManager);
+		return new DeferringMethodInterceptor<>(interceptor,
+				(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
 	}
 
 	@Bean
 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-	static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
-			MethodSecurityExpressionHandler expressionHandler,
+	static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
 			ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
 		PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(
 				expressionHandler);
-		defaultsObjectProvider.ifAvailable(interceptor::setTemplateDefaults);
-		return interceptor;
+		return new DeferringMethodInterceptor<>(interceptor,
+				(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
 	}
 
 	@Bean
 	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-	static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
+	static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
 			MethodSecurityExpressionHandler expressionHandler,
 			ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
 			ObjectProvider<ObservationRegistry> registryProvider) {
 		PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
 				expressionHandler);
 		ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
-		defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults);
-		return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
+		AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
+			.postAuthorize(authorizationManager);
+		return new DeferringMethodInterceptor<>(interceptor,
+				(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
 	}
 
 	@Bean
@@ -112,4 +125,50 @@ final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
 		return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
 	}
 
+	private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
+			implements AuthorizationAdvisor {
+
+		private final Pointcut pointcut;
+
+		private final int order;
+
+		private final Supplier<M> delegate;
+
+		DeferringMethodInterceptor(M delegate, Consumer<M> supplier) {
+			this.pointcut = delegate.getPointcut();
+			this.order = delegate.getOrder();
+			this.delegate = SingletonSupplier.of(() -> {
+				supplier.accept(delegate);
+				return delegate;
+			});
+		}
+
+		@Nullable
+		@Override
+		public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
+			return this.delegate.get().invoke(invocation);
+		}
+
+		@Override
+		public Pointcut getPointcut() {
+			return this.pointcut;
+		}
+
+		@Override
+		public Advice getAdvice() {
+			return this;
+		}
+
+		@Override
+		public int getOrder() {
+			return this.order;
+		}
+
+		@Override
+		public boolean isPerInstance() {
+			return true;
+		}
+
+	}
+
 }

+ 45 - 0
config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java

@@ -0,0 +1,45 @@
+/*
+ * 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.config.annotation.method.configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.aop.framework.AopInfrastructureBean;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Role;
+import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
+
+@Configuration(proxyBeanMethods = false)
+final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
+
+	@Bean
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	static ReactiveAuthorizationAdvisorProxyFactory authorizationProxyFactory(
+			ObjectProvider<AuthorizationAdvisor> provider) {
+		List<AuthorizationAdvisor> advisors = new ArrayList<>();
+		provider.forEach(advisors::add);
+		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
+		factory.setAdvisors(advisors);
+		return factory;
+	}
+
+}

+ 3 - 1
config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java

@@ -51,13 +51,15 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
 		else {
 			imports.add(ReactiveMethodSecurityConfiguration.class.getName());
 		}
+		imports.add(ReactiveAuthorizationProxyConfiguration.class.getName());
 		return imports.toArray(new String[0]);
 	}
 
 	private static final class AutoProxyRegistrarSelector
 			extends AdviceModeImportSelector<EnableReactiveMethodSecurity> {
 
-		private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() };
+		private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName(),
+				MethodSecurityAdvisorRegistrar.class.getName() };
 
 		@Override
 		protected String[] selectImports(@NonNull AdviceMode adviceMode) {

+ 45 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java

@@ -18,15 +18,20 @@ package org.springframework.security.config.annotation.method.configuration;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.prepost.PostAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.TestAuthentication;
 import org.springframework.security.authorization.AuthorizationProxyFactory;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
 import org.springframework.security.test.context.support.WithMockUser;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -69,12 +74,42 @@ public class AuthorizationProxyConfigurationTests {
 		assertThat(toaster.extractBread()).isEqualTo("yummy");
 	}
 
+	@Test
+	public void proxyReactiveWhenNotPreAuthorizedThenDenies() {
+		this.spring.register(ReactiveDefaultsConfig.class).autowire();
+		Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
+		Authentication user = TestAuthentication.authenticatedUser();
+		StepVerifier
+			.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
+			.verifyError(AccessDeniedException.class);
+		StepVerifier
+			.create(toaster.reactiveExtractBread().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
+			.verifyError(AccessDeniedException.class);
+	}
+
+	@Test
+	public void proxyReactiveWhenPreAuthorizedThenAllows() {
+		this.spring.register(ReactiveDefaultsConfig.class).autowire();
+		Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
+		Authentication admin = TestAuthentication.authenticatedAdmin();
+		StepVerifier
+			.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(admin)))
+			.expectNext()
+			.verifyComplete();
+	}
+
 	@EnableMethodSecurity
 	@Configuration
 	static class DefaultsConfig {
 
 	}
 
+	@EnableReactiveMethodSecurity
+	@Configuration
+	static class ReactiveDefaultsConfig {
+
+	}
+
 	static class Toaster {
 
 		@PreAuthorize("hasRole('ADMIN')")
@@ -87,6 +122,16 @@ public class AuthorizationProxyConfigurationTests {
 			return "yummy";
 		}
 
+		@PreAuthorize("hasRole('ADMIN')")
+		Mono<Void> reactiveMakeToast() {
+			return Mono.empty();
+		}
+
+		@PostAuthorize("hasRole('ADMIN')")
+		Mono<String> reactiveExtractBread() {
+			return Mono.just("yummy");
+		}
+
 	}
 
 }

+ 137 - 0
core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java

@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.aop.framework.ProxyFactory;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
+import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
+import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
+import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor;
+import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
+
+/**
+ * A proxy factory for applying authorization advice to an arbitrary object.
+ *
+ * <p>
+ * For example, consider a non-Spring-managed object {@code Foo}: <pre>
+ *     class Foo {
+ *         &#064;PreAuthorize("hasAuthority('bar:read')")
+ *         String bar() { ... }
+ *     }
+ * </pre>
+ *
+ * Use {@link ReactiveAuthorizationAdvisorProxyFactory} to wrap the instance in Spring
+ * Security's {@link org.springframework.security.access.prepost.PreAuthorize} method
+ * interceptor like so:
+ *
+ * <pre>
+ *     AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
+ *     AuthorizationProxyFactory proxyFactory = new AuthorizationProxyFactory(preAuthorize);
+ *     Foo foo = new Foo();
+ *     foo.bar(); // passes
+ *     Foo securedFoo = proxyFactory.proxy(foo);
+ *     securedFoo.bar(); // access denied!
+ * </pre>
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ */
+public final class ReactiveAuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory {
+
+	private final AuthorizationAdvisorProxyFactory defaults = new AuthorizationAdvisorProxyFactory();
+
+	public ReactiveAuthorizationAdvisorProxyFactory() {
+		List<AuthorizationAdvisor> advisors = new ArrayList<>();
+		advisors.add(AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize());
+		advisors.add(AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize());
+		advisors.add(new PreFilterAuthorizationReactiveMethodInterceptor());
+		advisors.add(new PostFilterAuthorizationReactiveMethodInterceptor());
+		this.defaults.setAdvisors(advisors);
+	}
+
+	/**
+	 * Proxy an object to enforce authorization advice.
+	 *
+	 * <p>
+	 * Proxies any instance of a non-final class or a class that implements more than one
+	 * interface.
+	 *
+	 * <p>
+	 * If {@code target} is an {@link Iterator}, {@link Collection}, {@link Array},
+	 * {@link Map}, {@link Stream}, or {@link Optional}, then the element or value type is
+	 * proxied.
+	 *
+	 * <p>
+	 * If {@code target} is a {@link Class}, then {@link ProxyFactory#getProxyClass} is
+	 * invoked instead.
+	 * @param target the instance to proxy
+	 * @return the proxied instance
+	 */
+	@Override
+	public Object proxy(Object target) {
+		if (target instanceof Mono<?> mono) {
+			return proxyMono(mono);
+		}
+		if (target instanceof Flux<?> flux) {
+			return proxyFlux(flux);
+		}
+		return this.defaults.proxy(target);
+	}
+
+	/**
+	 * Add advisors that should be included to each proxy created.
+	 *
+	 * <p>
+	 * All advisors are re-sorted by their advisor order.
+	 * @param advisors the advisors to add
+	 */
+	public void setAdvisors(AuthorizationAdvisor... advisors) {
+		this.defaults.setAdvisors(advisors);
+	}
+
+	/**
+	 * Add advisors that should be included to each proxy created.
+	 *
+	 * <p>
+	 * All advisors are re-sorted by their advisor order.
+	 * @param advisors the advisors to add
+	 */
+	public void setAdvisors(Collection<AuthorizationAdvisor> advisors) {
+		this.defaults.setAdvisors(advisors);
+	}
+
+	private Mono<?> proxyMono(Mono<?> mono) {
+		return mono.map(this::proxy);
+	}
+
+	private Flux<?> proxyFlux(Flux<?> flux) {
+		return flux.map(this::proxy);
+	}
+
+}

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

@@ -28,11 +28,8 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.core.KotlinDetector;
 import org.springframework.core.MethodParameter;
-import org.springframework.core.Ordered;
 import org.springframework.core.ReactiveAdapter;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.security.access.prepost.PostAuthorize;
@@ -48,8 +45,7 @@ import org.springframework.util.Assert;
  * @author Evgeniy Cheban
  * @since 5.8
  */
-public final class AuthorizationManagerAfterReactiveMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class AuthorizationManagerAfterReactiveMethodInterceptor implements AuthorizationAdvisor {
 
 	private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
 

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

@@ -27,11 +27,8 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.core.KotlinDetector;
 import org.springframework.core.MethodParameter;
-import org.springframework.core.Ordered;
 import org.springframework.core.ReactiveAdapter;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -48,8 +45,7 @@ import org.springframework.util.Assert;
  * @author Josh Cummings
  * @since 5.8
  */
-public final class AuthorizationManagerBeforeReactiveMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class AuthorizationManagerBeforeReactiveMethodInterceptor implements AuthorizationAdvisor {
 
 	private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
 

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

@@ -26,9 +26,6 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
-import org.springframework.core.Ordered;
 import org.springframework.core.ReactiveAdapter;
 import org.springframework.core.ReactiveAdapterRegistry;
 import org.springframework.expression.EvaluationContext;
@@ -46,8 +43,7 @@ import org.springframework.util.Assert;
  * @author Evgeniy Cheban
  * @since 5.8
  */
-public final class PostFilterAuthorizationReactiveMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class PostFilterAuthorizationReactiveMethodInterceptor implements AuthorizationAdvisor {
 
 	private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
 

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

@@ -26,10 +26,7 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import org.springframework.aop.Pointcut;
-import org.springframework.aop.PointcutAdvisor;
-import org.springframework.aop.framework.AopInfrastructureBean;
 import org.springframework.aop.support.AopUtils;
-import org.springframework.core.Ordered;
 import org.springframework.core.ParameterNameDiscoverer;
 import org.springframework.core.ReactiveAdapter;
 import org.springframework.core.ReactiveAdapterRegistry;
@@ -50,8 +47,7 @@ import org.springframework.util.StringUtils;
  * @author Evgeniy Cheban
  * @since 5.8
  */
-public final class PreFilterAuthorizationReactiveMethodInterceptor
-		implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
+public final class PreFilterAuthorizationReactiveMethodInterceptor implements AuthorizationAdvisor {
 
 	private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
 

+ 226 - 0
core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java

@@ -0,0 +1,226 @@
+/*
+ * 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;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import org.springframework.aop.Pointcut;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.TestAuthentication;
+import org.springframework.security.authorization.method.AuthorizationAdvisor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class ReactiveAuthorizationAdvisorProxyFactoryTests {
+
+	private final Authentication user = TestAuthentication.authenticatedUser();
+
+	private final Authentication admin = TestAuthentication.authenticatedAdmin();
+
+	private final Flight flight = new Flight();
+
+	private final User alan = new User("alan", "alan", "turing");
+
+	@Test
+	public void proxyWhenPreAuthorizeThenHonors() {
+		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
+		Flight flight = new Flight();
+		StepVerifier
+			.create(flight.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.expectNext(35000d)
+			.verifyComplete();
+		Flight secured = proxy(factory, flight);
+		StepVerifier
+			.create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.verifyError(AccessDeniedException.class);
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeOnInterfaceThenHonors() {
+		SecurityContextHolder.getContext().setAuthentication(this.user);
+		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
+		StepVerifier
+			.create(this.alan.getFirstName().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.expectNext("alan")
+			.verifyComplete();
+		User secured = proxy(factory, this.alan);
+		StepVerifier
+			.create(secured.getFirstName().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.verifyError(AccessDeniedException.class);
+		StepVerifier
+			.create(secured.getFirstName()
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authenticated("alan"))))
+			.expectNext("alan")
+			.verifyComplete();
+		StepVerifier
+			.create(secured.getFirstName().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.admin)))
+			.expectNext("alan")
+			.verifyComplete();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeOnRecordThenHonors() {
+		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
+		HasSecret repo = new Repository(Mono.just("secret"));
+		StepVerifier.create(repo.secret().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.expectNext("secret")
+			.verifyComplete();
+		HasSecret secured = proxy(factory, repo);
+		StepVerifier.create(secured.secret().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.verifyError(AccessDeniedException.class);
+		StepVerifier.create(secured.secret().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.admin)))
+			.expectNext("secret")
+			.verifyComplete();
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeOnFluxThenHonors() {
+		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
+		Flux<Flight> flights = Flux.just(this.flight);
+		Flux<Flight> secured = proxy(factory, flights);
+		StepVerifier
+			.create(secured.flatMap(Flight::getAltitude)
+				.contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.verifyError(AccessDeniedException.class);
+	}
+
+	@Test
+	public void proxyWhenPreAuthorizeForClassThenHonors() {
+		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
+		Class<Flight> clazz = proxy(factory, Flight.class);
+		assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0");
+		Flight secured = proxy(factory, this.flight);
+		StepVerifier
+			.create(secured.getAltitude().contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.user)))
+			.verifyError(AccessDeniedException.class);
+	}
+
+	@Test
+	public void setAdvisorsWhenProxyThenVisits() {
+		AuthorizationAdvisor advisor = mock(AuthorizationAdvisor.class);
+		given(advisor.getAdvice()).willReturn(advisor);
+		given(advisor.getPointcut()).willReturn(Pointcut.TRUE);
+		ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
+		factory.setAdvisors(advisor);
+		Flight flight = proxy(factory, this.flight);
+		flight.getAltitude();
+		verify(advisor, atLeastOnce()).getPointcut();
+	}
+
+	private Authentication authenticated(String user, String... authorities) {
+		return TestAuthentication.authenticated(TestAuthentication.withUsername(user).authorities(authorities).build());
+	}
+
+	private <T> T proxy(AuthorizationProxyFactory factory, Object target) {
+		return (T) factory.proxy(target);
+	}
+
+	static class Flight {
+
+		@PreAuthorize("hasRole('PILOT')")
+		Mono<Double> getAltitude() {
+			return Mono.just(35000d);
+		}
+
+	}
+
+	interface Identifiable {
+
+		@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
+		Mono<String> getFirstName();
+
+		@PreAuthorize("authentication.name == this.id || hasRole('ADMIN')")
+		Mono<String> getLastName();
+
+	}
+
+	public static class User implements Identifiable, Comparable<User> {
+
+		private final String id;
+
+		private final String firstName;
+
+		private final String lastName;
+
+		User(String id, String firstName, String lastName) {
+			this.id = id;
+			this.firstName = firstName;
+			this.lastName = lastName;
+		}
+
+		public String getId() {
+			return this.id;
+		}
+
+		@Override
+		public Mono<String> getFirstName() {
+			return Mono.just(this.firstName);
+		}
+
+		@Override
+		public Mono<String> getLastName() {
+			return Mono.just(this.lastName);
+		}
+
+		@Override
+		public int compareTo(@NotNull User that) {
+			return this.id.compareTo(that.getId());
+		}
+
+	}
+
+	static class UserRepository implements Iterable<User> {
+
+		List<User> users = List.of(new User("1", "first", "last"));
+
+		Flux<User> findAll() {
+			return Flux.fromIterable(this.users);
+		}
+
+		@NotNull
+		@Override
+		public Iterator<User> iterator() {
+			return this.users.iterator();
+		}
+
+	}
+
+	interface HasSecret {
+
+		Mono<String> secret();
+
+	}
+
+	record Repository(@PreAuthorize("hasRole('ADMIN')") Mono<String> secret) implements HasSecret {
+	}
+
+}