From c611b7e33b9ffcf40141b4bce0ac8a1f557434ff Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 14 Mar 2024 10:44:06 -0600 Subject: [PATCH] Add AuthorizationProxyFactory Reactive Support Issue gh-14596 --- ...ionManagerMethodSecurityConfiguration.java | 93 +++++-- ...activeAuthorizationProxyConfiguration.java | 45 ++++ .../ReactiveMethodSecuritySelector.java | 4 +- .../AuthorizationProxyConfigurationTests.java | 45 ++++ ...ctiveAuthorizationAdvisorProxyFactory.java | 137 +++++++++++ ...ManagerAfterReactiveMethodInterceptor.java | 6 +- ...anagerBeforeReactiveMethodInterceptor.java | 6 +- ...uthorizationReactiveMethodInterceptor.java | 6 +- ...uthorizationReactiveMethodInterceptor.java | 6 +- ...AuthorizationAdvisorProxyFactoryTests.java | 226 ++++++++++++++++++ 10 files changed, 536 insertions(+), 38 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java create mode 100644 core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java create mode 100644 core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index 848934bb25..2bfa745f67 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/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 io.micrometer.observation.ObservationRegistry; -import org.aopalliance.intercept.MethodInvocation; +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 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 defaultsObjectProvider, ObjectProvider registryProvider) { PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager( expressionHandler); - defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults); ReactiveAuthorizationManager 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 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 defaultsObjectProvider, ObjectProvider registryProvider) { PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager( expressionHandler); ReactiveAuthorizationManager 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 + implements AuthorizationAdvisor { + + private final Pointcut pointcut; + + private final int order; + + private final Supplier delegate; + + DeferringMethodInterceptor(M delegate, Consumer 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; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java new file mode 100644 index 0000000000..b7e3b07fbd --- /dev/null +++ b/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 provider) { + List advisors = new ArrayList<>(); + provider.forEach(advisors::add); + ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory(); + factory.setAdvisors(advisors); + return factory; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java index dd984bc1a8..b1c923383e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java +++ b/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 { - 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) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java index e3e41c0316..254c8b0871 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java +++ b/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 reactiveMakeToast() { + return Mono.empty(); + } + + @PostAuthorize("hasRole('ADMIN')") + Mono reactiveExtractBread() { + return Mono.just("yummy"); + } + } } diff --git a/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java new file mode 100644 index 0000000000..5720082862 --- /dev/null +++ b/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. + * + *

+ * For example, consider a non-Spring-managed object {@code Foo}:

+ *     class Foo {
+ *         @PreAuthorize("hasAuthority('bar:read')")
+ *         String bar() { ... }
+ *     }
+ * 
+ * + * Use {@link ReactiveAuthorizationAdvisorProxyFactory} to wrap the instance in Spring + * Security's {@link org.springframework.security.access.prepost.PreAuthorize} method + * interceptor like so: + * + *
+ *     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!
+ * 
+ * + * @author Josh Cummings + * @since 6.3 + */ +public final class ReactiveAuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory { + + private final AuthorizationAdvisorProxyFactory defaults = new AuthorizationAdvisorProxyFactory(); + + public ReactiveAuthorizationAdvisorProxyFactory() { + List 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. + * + *

+ * Proxies any instance of a non-final class or a class that implements more than one + * interface. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * All advisors are re-sorted by their advisor order. + * @param advisors the advisors to add + */ + public void setAdvisors(Collection 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); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java index 550b8fbdef..ea3d0236d8 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java +++ b/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"; diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java index f3d1cce8bf..25e43e1588 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java +++ b/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"; diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java index 19dbbb7e0b..072bb5b75d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java +++ b/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(); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java index 18f7d61123..19775eb126 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java +++ b/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(); diff --git a/core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactoryTests.java new file mode 100644 index 0000000000..1dc8afccdc --- /dev/null +++ b/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 flights = Flux.just(this.flight); + Flux 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 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 proxy(AuthorizationProxyFactory factory, Object target) { + return (T) factory.proxy(target); + } + + static class Flight { + + @PreAuthorize("hasRole('PILOT')") + Mono getAltitude() { + return Mono.just(35000d); + } + + } + + interface Identifiable { + + @PreAuthorize("authentication.name == this.id || hasRole('ADMIN')") + Mono getFirstName(); + + @PreAuthorize("authentication.name == this.id || hasRole('ADMIN')") + Mono getLastName(); + + } + + public static class User implements Identifiable, Comparable { + + 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 getFirstName() { + return Mono.just(this.firstName); + } + + @Override + public Mono getLastName() { + return Mono.just(this.lastName); + } + + @Override + public int compareTo(@NotNull User that) { + return this.id.compareTo(that.getId()); + } + + } + + static class UserRepository implements Iterable { + + List users = List.of(new User("1", "first", "last")); + + Flux findAll() { + return Flux.fromIterable(this.users); + } + + @NotNull + @Override + public Iterator iterator() { + return this.users.iterator(); + } + + } + + interface HasSecret { + + Mono secret(); + + } + + record Repository(@PreAuthorize("hasRole('ADMIN')") Mono secret) implements HasSecret { + } + +}