From 769ed435357c38479283edf310511f06d98fb601 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 14 Mar 2024 17:03:48 -0600 Subject: [PATCH] Polish AuthorizeReturnObject SUpport - Remove dependency on concrete implementation - Simplify AuthorizationAdvisorProxyFactory construction - Add ability to configure alternative proxying behaviors --- .../AuthorizationProxyConfiguration.java | 49 +++- ...activeAuthorizationProxyConfiguration.java | 85 +++++++ .../ReactiveMethodSecuritySelector.java | 2 +- ...ePostMethodSecurityConfigurationTests.java | 9 +- ...ctiveMethodSecurityConfigurationTests.java | 11 +- .../AuthorizationAdvisorProxyFactory.java | 109 +++------ .../AuthorizationProxyFactoryPredicate.java | 58 +++++ ...ctiveAuthorizationAdvisorProxyFactory.java | 137 +++++++++++ ...kipAuthorizationProxyFactoryPredicate.java | 68 ++++++ .../AuthorizationProxyMethodInterceptor.java | 5 +- ...AuthorizationAdvisorProxyFactoryTests.java | 85 ++----- ...AuthorizationAdvisorProxyFactoryTests.java | 226 ++++++++++++++++++ 12 files changed, 683 insertions(+), 161 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/AuthorizationProxyFactoryPredicate.java create mode 100644 core/src/main/java/org/springframework/security/authorization/ReactiveAuthorizationAdvisorProxyFactory.java create mode 100644 core/src/main/java/org/springframework/security/authorization/SkipAuthorizationProxyFactoryPredicate.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/AuthorizationProxyConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java index 04276bce1a9..5d1509d9138 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java @@ -27,32 +27,59 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory; +import org.springframework.security.authorization.AuthorizationProxyFactory; +import org.springframework.security.authorization.AuthorizationProxyFactoryPredicate; import org.springframework.security.authorization.method.AuthorizationAdvisor; import org.springframework.security.authorization.method.AuthorizationProxyMethodInterceptor; -import org.springframework.security.config.Customizer; @Configuration(proxyBeanMethods = false) final class AuthorizationProxyConfiguration implements AopInfrastructureBean { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider provider, - ObjectProvider> customizers) { + static DelegatingAuthorizationProxyFactory authorizationProxyFactory(ObjectProvider provider, + List predicates) { + DelegatingAuthorizationProxyFactory delegating = new DelegatingAuthorizationProxyFactory(predicates); List advisors = new ArrayList<>(); provider.forEach(advisors::add); - AnnotationAwareOrderComparator.sort(advisors); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(advisors); - customizers.forEach((customizer) -> customizer.customize(factory)); - return factory; + delegating.defaults.setAdvisors(advisors); + return delegating; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static MethodInterceptor authorizationProxyMethodInterceptor( - AuthorizationAdvisorProxyFactory authorizationProxyFactory) { - return new AuthorizationProxyMethodInterceptor(authorizationProxyFactory); + static MethodInterceptor authorizationProxyMethodInterceptor(ObjectProvider provider, + DelegatingAuthorizationProxyFactory authorizationProxyFactory) { + AuthorizationProxyMethodInterceptor interceptor = new AuthorizationProxyMethodInterceptor( + authorizationProxyFactory); + List advisors = new ArrayList<>(); + provider.forEach(advisors::add); + advisors.add(interceptor); + authorizationProxyFactory.defaults.setAdvisors(advisors); + return interceptor; + } + + private static class DelegatingAuthorizationProxyFactory implements AuthorizationProxyFactory { + + private final List predicates; + + private final AuthorizationAdvisorProxyFactory defaults = new AuthorizationAdvisorProxyFactory(); + + DelegatingAuthorizationProxyFactory(List predicates) { + this.predicates = new ArrayList<>(predicates); + } + + @Override + public Object proxy(Object object) { + for (AuthorizationProxyFactoryPredicate factory : this.predicates) { + if (factory.proxies(object)) { + return factory.proxy(object); + } + } + return this.defaults.proxy(object); + } + } } 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 00000000000..1067f771283 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java @@ -0,0 +1,85 @@ +/* + * 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.aopalliance.intercept.MethodInterceptor; + +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.AuthorizationProxyFactory; +import org.springframework.security.authorization.AuthorizationProxyFactoryPredicate; +import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory; +import org.springframework.security.authorization.method.AuthorizationAdvisor; +import org.springframework.security.authorization.method.AuthorizationProxyMethodInterceptor; + +@Configuration(proxyBeanMethods = false) +final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static DelegatingAuthorizationProxyFactory authorizationProxyFactory(ObjectProvider provider, + List predicates) { + DelegatingAuthorizationProxyFactory delegating = new DelegatingAuthorizationProxyFactory(predicates); + List advisors = new ArrayList<>(); + provider.forEach(advisors::add); + delegating.defaults.setAdvisors(advisors); + return delegating; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static MethodInterceptor authorizationProxyMethodInterceptor(ObjectProvider provider, + DelegatingAuthorizationProxyFactory authorizationProxyFactory) { + AuthorizationProxyMethodInterceptor interceptor = new AuthorizationProxyMethodInterceptor( + authorizationProxyFactory); + List advisors = new ArrayList<>(); + provider.forEach(advisors::add); + advisors.add(interceptor); + authorizationProxyFactory.defaults.setAdvisors(advisors); + return interceptor; + } + + private static class DelegatingAuthorizationProxyFactory implements AuthorizationProxyFactory { + + private final List predicates; + + private final ReactiveAuthorizationAdvisorProxyFactory defaults = new ReactiveAuthorizationAdvisorProxyFactory(); + + DelegatingAuthorizationProxyFactory(List predicates) { + this.predicates = new ArrayList<>(predicates); + } + + @Override + public Object proxy(Object object) { + for (AuthorizationProxyFactoryPredicate factory : this.predicates) { + if (factory.proxies(object)) { + return factory.proxy(object); + } + } + return this.defaults.proxy(object); + } + + } + +} 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 224d388b383..b1c923383e5 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,7 +51,7 @@ public String[] selectImports(AnnotationMetadata importMetadata) { else { imports.add(ReactiveMethodSecurityConfiguration.class.getName()); } - imports.add(AuthorizationProxyConfiguration.class.getName()); + imports.add(ReactiveAuthorizationProxyConfiguration.class.getName()); return imports.toArray(new String[0]); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 8f2f8b76c9a..5a71fee71c8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -58,16 +58,16 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; -import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationProxyFactoryPredicate; +import org.springframework.security.authorization.SkipAuthorizationProxyFactoryPredicate; 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.MethodInvocationResult; import org.springframework.security.authorization.method.PrePostTemplateDefaults; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.test.SpringTestContext; @@ -1146,8 +1146,9 @@ List resultsContainDave(List list) { static class AuthorizeResultConfig { @Bean - static Customizer returnObject() { - return (proxyFactory) -> proxyFactory.setSkipProxy(AuthorizationAdvisorProxyFactory.SKIP_VALUE_TYPES); + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static AuthorizationProxyFactoryPredicate skipValueTypes() { + return SkipAuthorizationProxyFactoryPredicate.skipValueTypes(); } @Bean diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java index 59257356840..b8c5715c28c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java @@ -30,8 +30,10 @@ import reactor.test.StepVerifier; import org.springframework.beans.factory.annotation.Autowired; +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.expression.EvaluationContext; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.expression.SecurityExpressionRoot; @@ -42,9 +44,9 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.authentication.TestAuthentication; -import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory; +import org.springframework.security.authorization.AuthorizationProxyFactoryPredicate; +import org.springframework.security.authorization.SkipAuthorizationProxyFactoryPredicate; import org.springframework.security.authorization.method.AuthorizeReturnObject; -import org.springframework.security.config.Customizer; import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; @@ -241,8 +243,9 @@ public void bar(String param) { static class AuthorizeResultConfig { @Bean - static Customizer returnObject() { - return (proxyFactory) -> proxyFactory.setSkipProxy(AuthorizationAdvisorProxyFactory.SKIP_VALUE_TYPES); + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static AuthorizationProxyFactoryPredicate skipValueTypes() { + return SkipAuthorizationProxyFactoryPredicate.skipValueTypes(); } @Bean diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java index 1d73fac38ad..ceffb7ded11 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactory.java @@ -34,17 +34,16 @@ import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; -import java.util.function.Predicate; import java.util.stream.Stream; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.aop.Advisor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.security.authorization.method.AuthorizationAdvisor; -import org.springframework.util.Assert; +import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; +import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; import org.springframework.util.ClassUtils; /** @@ -76,39 +75,15 @@ */ public final class AuthorizationAdvisorProxyFactory implements AuthorizationProxyFactory { - private static final boolean isReactivePresent = ClassUtils.isPresent("reactor.core.publisher.Mono", null); - - public static final Predicate> SKIP_VALUE_TYPES = ClassUtils::isSimpleValueType; - - private final Collection advisors; - - private Predicate> skipProxy = (returnType) -> false; - - public AuthorizationAdvisorProxyFactory(AuthorizationAdvisor... advisors) { - this.advisors = List.of(advisors); - } + private List advisors = new ArrayList<>(); - public AuthorizationAdvisorProxyFactory(Collection advisors) { - this.advisors = List.copyOf(advisors); - } - - /** - * Create a new {@link AuthorizationAdvisorProxyFactory} that includes the given - * advisors in addition to any advisors {@code this} instance already has. - * - *

- * All advisors are re-sorted by their advisor order. - * @param advisors the advisors to add - * @return a new {@link AuthorizationAdvisorProxyFactory} instance - */ - public AuthorizationAdvisorProxyFactory withAdvisors(AuthorizationAdvisor... advisors) { - List merged = new ArrayList<>(this.advisors.size() + advisors.length); - merged.addAll(this.advisors); - merged.addAll(List.of(advisors)); - AnnotationAwareOrderComparator.sort(merged); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(merged); - factory.setSkipProxy(this.skipProxy); - return factory; + public AuthorizationAdvisorProxyFactory() { + List advisors = new ArrayList<>(); + advisors.add(AuthorizationManagerBeforeMethodInterceptor.preAuthorize()); + advisors.add(AuthorizationManagerAfterMethodInterceptor.postAuthorize()); + advisors.add(new PreFilterAuthorizationMethodInterceptor()); + advisors.add(new PostFilterAuthorizationMethodInterceptor()); + setAdvisors(advisors); } /** @@ -134,9 +109,6 @@ public Object proxy(Object target) { if (target == null) { return null; } - if (this.skipProxy.test(target.getClass())) { - return target; - } if (target instanceof Class targetClass) { return proxyClass(targetClass); } @@ -173,14 +145,6 @@ public Object proxy(Object target) { if (target instanceof Optional optional) { return proxyOptional(optional); } - if (isReactivePresent) { - if (MonoProxySupport.isMono(target)) { - return MonoProxySupport.proxy(target, this); - } - if (FluxProxySupport.isFlux(target)) { - return FluxProxySupport.proxy(target, this); - } - } ProxyFactory factory = new ProxyFactory(target); for (Advisor advisor : this.advisors) { factory.addAdvisors(advisor); @@ -189,9 +153,28 @@ public Object proxy(Object target) { return factory.getProxy(); } - public void setSkipProxy(Predicate> skipProxy) { - Assert.notNull(skipProxy, "skipProxy must not be null"); - this.skipProxy = skipProxy; + /** + * 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.advisors = new ArrayList<>(List.of(advisors)); + AnnotationAwareOrderComparator.sort(this.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.advisors = new ArrayList<>(advisors); + AnnotationAwareOrderComparator.sort(this.advisors); } @SuppressWarnings("unchecked") @@ -334,28 +317,4 @@ private Optional proxyOptional(Optional optional) { return optional.map(this::proxy); } - private static final class MonoProxySupport { - - static boolean isMono(Object target) { - return target instanceof Mono; - } - - static Mono proxy(Object target, AuthorizationProxyFactory factory) { - return ((Mono) target).map(factory::proxy); - } - - } - - private static final class FluxProxySupport { - - static boolean isFlux(Object target) { - return target instanceof Flux; - } - - static Flux proxy(Object target, AuthorizationProxyFactory factory) { - return ((Flux) target).map(factory::proxy); - } - - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactoryPredicate.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactoryPredicate.java new file mode 100644 index 00000000000..dbbf615b777 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationProxyFactoryPredicate.java @@ -0,0 +1,58 @@ +/* + * 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; + +/** + * A handy interface for indicating specialized proxy behavior for a given set of classes + * or objects. + * + *

+ * Each {@link AuthorizationProxyFactoryPredicate} is picked up by Spring Security and + * consulted before delegating to its default {@link AuthorizationProxyFactory}. + * + *

+ * Implementations may choose to delegate to an {@link AuthorizationProxyFactory} or can + * handle the proxying themselves. + * + * @author Josh Cummings + * @since 6.3 + * @see SkipAuthorizationProxyFactoryPredicate + */ +public interface AuthorizationProxyFactoryPredicate { + + /** + * Wrap the given {@code object} in authorization-related advice. + * + *

+ * Please check the implementation for which kinds of objects it supports. + * @param object the object to proxy + * @return the proxied object + * @throws org.springframework.aop.framework.AopConfigException if a proxy cannot be + * created + */ + Object proxy(Object object); + + /** + * Say whether this object can be proxied by this instance + * @param object the object needing proxying + * @return whether this instance can proxy the {@code object} + */ + default boolean proxies(Object object) { + return true; + } + +} 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 00000000000..5720082862d --- /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/SkipAuthorizationProxyFactoryPredicate.java b/core/src/main/java/org/springframework/security/authorization/SkipAuthorizationProxyFactoryPredicate.java new file mode 100644 index 00000000000..3f95206d00c --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/SkipAuthorizationProxyFactoryPredicate.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2022 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.function.Predicate; + +import org.springframework.util.ClassUtils; + +/** + * An implementation of {@link AuthorizationProxyFactoryPredicate} that skips proxying + * based on the given {@link Predicate} + * + * @author Josh Cummings + * @since 6.3 + */ +public final class SkipAuthorizationProxyFactoryPredicate implements AuthorizationProxyFactoryPredicate { + + private final Predicate> shouldSkip; + + public SkipAuthorizationProxyFactoryPredicate(Predicate> shouldSkip) { + this.shouldSkip = shouldSkip; + } + + /** + * Skip proxying any classes that are value types. See + * {@link ClassUtils#isSimpleValueType} for more information. + * + *

+ * When this instance is published as a bean, Spring Security's + * {@link AuthorizationProxyFactory} will skip proxying any value types. + * + *

+ * This can be helpful in conjunction with + * {@link org.springframework.security.authorization.method.AuthorizeReturnObject} + * when that annotation is placed at the class level. In this way, if some of the + * methods return a value type, they will be skipped. + * @return a {@link AuthorizationProxyFactoryPredicate} that skips proxying any value + * types + */ + public static SkipAuthorizationProxyFactoryPredicate skipValueTypes() { + return new SkipAuthorizationProxyFactoryPredicate(ClassUtils::isSimpleValueType); + } + + @Override + public Object proxy(Object object) { + return object; + } + + @Override + public boolean proxies(Object object) { + return this.shouldSkip.test(object.getClass()); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationProxyMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationProxyMethodInterceptor.java index 8185203a35b..f299599c08a 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationProxyMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationProxyMethodInterceptor.java @@ -25,7 +25,6 @@ import org.springframework.aop.Pointcut; import org.springframework.aop.support.Pointcuts; import org.springframework.aop.support.StaticMethodMatcherPointcut; -import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory; import org.springframework.security.authorization.AuthorizationProxyFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -40,9 +39,9 @@ public final class AuthorizationProxyMethodInterceptor implements AuthorizationA private int order = AuthorizationInterceptorsOrder.SECURE_RESULT.getOrder(); - public AuthorizationProxyMethodInterceptor(AuthorizationAdvisorProxyFactory authorizationProxyFactory) { + public AuthorizationProxyMethodInterceptor(AuthorizationProxyFactory authorizationProxyFactory) { Assert.notNull(authorizationProxyFactory, "authorizationManager cannot be null"); - this.authorizationProxyFactory = authorizationProxyFactory.withAdvisors(this); + this.authorizationProxyFactory = authorizationProxyFactory; } @Override diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java index 2a7852b28cb..9ca1ce9b4df 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationAdvisorProxyFactoryTests.java @@ -41,7 +41,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authorization.method.AuthorizationAdvisor; -import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -65,9 +64,7 @@ public class AuthorizationAdvisorProxyFactoryTests { @Test public void proxyWhenPreAuthorizeThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Flight flight = new Flight(); assertThat(flight.getAltitude()).isEqualTo(35000d); Flight secured = proxy(factory, flight); @@ -78,9 +75,7 @@ public void proxyWhenPreAuthorizeThenHonors() { @Test public void proxyWhenPreAuthorizeOnInterfaceThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); assertThat(this.alan.getFirstName()).isEqualTo("alan"); User secured = proxy(factory, this.alan); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured::getFirstName); @@ -94,9 +89,7 @@ public void proxyWhenPreAuthorizeOnInterfaceThenHonors() { @Test public void proxyWhenPreAuthorizeOnRecordThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); HasSecret repo = new Repository("secret"); assertThat(repo.secret()).isEqualTo("secret"); HasSecret secured = proxy(factory, repo); @@ -109,9 +102,7 @@ public void proxyWhenPreAuthorizeOnRecordThenHonors() { @Test public void proxyWhenImmutableListThenReturnsSecuredImmutableList() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); List flights = List.of(this.flight); List secured = proxy(factory, flights); secured.forEach( @@ -123,9 +114,7 @@ public void proxyWhenImmutableListThenReturnsSecuredImmutableList() { @Test public void proxyWhenImmutableSetThenReturnsSecuredImmutableSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Set flights = Set.of(this.flight); Set secured = proxy(factory, flights); secured.forEach( @@ -137,9 +126,7 @@ public void proxyWhenImmutableSetThenReturnsSecuredImmutableSet() { @Test public void proxyWhenQueueThenReturnsSecuredQueue() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Queue flights = new LinkedList<>(List.of(this.flight)); Queue secured = proxy(factory, flights); assertThat(flights.size()).isEqualTo(secured.size()); @@ -151,9 +138,7 @@ public void proxyWhenQueueThenReturnsSecuredQueue() { @Test public void proxyWhenImmutableSortedSetThenReturnsSecuredImmutableSortedSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedSet users = Collections.unmodifiableSortedSet(new TreeSet<>(Set.of(this.alan))); SortedSet secured = proxy(factory, users); secured @@ -165,9 +150,7 @@ public void proxyWhenImmutableSortedSetThenReturnsSecuredImmutableSortedSet() { @Test public void proxyWhenImmutableSortedMapThenReturnsSecuredImmutableSortedMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedMap users = Collections .unmodifiableSortedMap(new TreeMap<>(Map.of(this.alan.getId(), this.alan))); SortedMap secured = proxy(factory, users); @@ -180,9 +163,7 @@ public void proxyWhenImmutableSortedMapThenReturnsSecuredImmutableSortedMap() { @Test public void proxyWhenImmutableMapThenReturnsSecuredImmutableMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Map users = Map.of(this.alan.getId(), this.alan); Map secured = proxy(factory, users); secured.forEach( @@ -194,9 +175,7 @@ public void proxyWhenImmutableMapThenReturnsSecuredImmutableMap() { @Test public void proxyWhenMutableListThenReturnsSecuredMutableList() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); List flights = new ArrayList<>(List.of(this.flight)); List secured = proxy(factory, flights); secured.forEach( @@ -208,9 +187,7 @@ public void proxyWhenMutableListThenReturnsSecuredMutableList() { @Test public void proxyWhenMutableSetThenReturnsSecuredMutableSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Set flights = new HashSet<>(Set.of(this.flight)); Set secured = proxy(factory, flights); secured.forEach( @@ -222,9 +199,7 @@ public void proxyWhenMutableSetThenReturnsSecuredMutableSet() { @Test public void proxyWhenMutableSortedSetThenReturnsSecuredMutableSortedSet() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedSet users = new TreeSet<>(Set.of(this.alan)); SortedSet secured = proxy(factory, users); secured.forEach((u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); @@ -235,9 +210,7 @@ public void proxyWhenMutableSortedSetThenReturnsSecuredMutableSortedSet() { @Test public void proxyWhenMutableSortedMapThenReturnsSecuredMutableSortedMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); SortedMap users = new TreeMap<>(Map.of(this.alan.getId(), this.alan)); SortedMap secured = proxy(factory, users); secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); @@ -248,9 +221,7 @@ public void proxyWhenMutableSortedMapThenReturnsSecuredMutableSortedMap() { @Test public void proxyWhenMutableMapThenReturnsSecuredMutableMap() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Map users = new HashMap<>(Map.of(this.alan.getId(), this.alan)); Map secured = proxy(factory, users); secured.forEach((id, u) -> assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(u::getFirstName)); @@ -261,9 +232,7 @@ public void proxyWhenMutableMapThenReturnsSecuredMutableMap() { @Test public void proxyWhenPreAuthorizeForOptionalThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Optional flights = Optional.of(this.flight); assertThat(flights.get().getAltitude()).isEqualTo(35000d); Optional secured = proxy(factory, flights); @@ -274,9 +243,7 @@ public void proxyWhenPreAuthorizeForOptionalThenHonors() { @Test public void proxyWhenPreAuthorizeForStreamThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Stream flights = Stream.of(this.flight); Stream secured = proxy(factory, flights); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(Flight::getAltitude)); @@ -286,9 +253,7 @@ public void proxyWhenPreAuthorizeForStreamThenHonors() { @Test public void proxyWhenPreAuthorizeForArrayThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Flight[] flights = { this.flight }; Flight[] secured = proxy(factory, flights); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(secured[0]::getAltitude); @@ -298,9 +263,7 @@ public void proxyWhenPreAuthorizeForArrayThenHonors() { @Test public void proxyWhenPreAuthorizeForIteratorThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Iterator flights = List.of(this.flight).iterator(); Iterator secured = proxy(factory, flights); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.next().getAltitude()); @@ -310,9 +273,7 @@ public void proxyWhenPreAuthorizeForIteratorThenHonors() { @Test public void proxyWhenPreAuthorizeForIterableThenHonors() { SecurityContextHolder.getContext().setAuthentication(this.user); - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Iterable users = new UserRepository(); Iterable secured = proxy(factory, users); assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> secured.forEach(User::getFirstName)); @@ -321,9 +282,7 @@ public void proxyWhenPreAuthorizeForIterableThenHonors() { @Test public void proxyWhenPreAuthorizeForClassThenHonors() { - AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor - .preAuthorize(); - AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(preAuthorize); + AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); Class clazz = proxy(factory, Flight.class); assertThat(clazz.getSimpleName()).contains("SpringCGLIB$$0"); Flight secured = proxy(factory, this.flight); @@ -334,12 +293,12 @@ public void proxyWhenPreAuthorizeForClassThenHonors() { } @Test - public void withAdvisorsWhenProxyThenVisits() { + public void setAdvisorsWhenProxyThenVisits() { AuthorizationAdvisor advisor = mock(AuthorizationAdvisor.class); given(advisor.getAdvice()).willReturn(advisor); given(advisor.getPointcut()).willReturn(Pointcut.TRUE); AuthorizationAdvisorProxyFactory factory = new AuthorizationAdvisorProxyFactory(); - factory = factory.withAdvisors(advisor); + factory.setAdvisors(advisor); Flight flight = proxy(factory, this.flight); flight.getAltitude(); verify(advisor, atLeastOnce()).getPointcut(); 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 00000000000..1dc8afccdce --- /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 { + } + +}