Skip to content

Commit c611b7e

Browse files
committed
Add AuthorizationProxyFactory Reactive Support
Issue gh-14596
1 parent f541bce commit c611b7e

File tree

10 files changed

+534
-36
lines changed

10 files changed

+534
-36
lines changed

config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,18 @@
1616

1717
package org.springframework.security.config.annotation.method.configuration;
1818

19+
import java.util.function.Consumer;
20+
import java.util.function.Supplier;
21+
1922
import io.micrometer.observation.ObservationRegistry;
23+
import org.aopalliance.aop.Advice;
24+
import org.aopalliance.intercept.MethodInterceptor;
2025
import org.aopalliance.intercept.MethodInvocation;
26+
import org.jetbrains.annotations.NotNull;
27+
import org.jetbrains.annotations.Nullable;
2128

29+
import org.springframework.aop.Pointcut;
30+
import org.springframework.aop.framework.AopInfrastructureBean;
2231
import org.springframework.beans.factory.ObjectProvider;
2332
import org.springframework.beans.factory.annotation.Autowired;
2433
import org.springframework.beans.factory.config.BeanDefinition;
@@ -29,6 +38,7 @@
2938
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
3039
import org.springframework.security.authentication.ReactiveAuthenticationManager;
3140
import org.springframework.security.authorization.ReactiveAuthorizationManager;
41+
import org.springframework.security.authorization.method.AuthorizationAdvisor;
3242
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
3343
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
3444
import org.springframework.security.authorization.method.MethodInvocationResult;
@@ -38,6 +48,7 @@
3848
import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor;
3949
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
4050
import org.springframework.security.config.core.GrantedAuthorityDefaults;
51+
import org.springframework.util.function.SingletonSupplier;
4152

4253
/**
4354
* Configuration for a {@link ReactiveAuthenticationManager} based Method Security.
@@ -46,54 +57,56 @@
4657
* @since 5.8
4758
*/
4859
@Configuration(proxyBeanMethods = false)
49-
final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
60+
final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements AopInfrastructureBean {
5061

5162
@Bean
5263
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
53-
static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
54-
MethodSecurityExpressionHandler expressionHandler,
64+
static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
5565
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
5666
PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(
5767
expressionHandler);
58-
defaultsObjectProvider.ifAvailable(interceptor::setTemplateDefaults);
59-
return interceptor;
68+
return new DeferringMethodInterceptor<>(interceptor,
69+
(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
6070
}
6171

6272
@Bean
6373
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
64-
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
74+
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
6575
MethodSecurityExpressionHandler expressionHandler,
6676
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
6777
ObjectProvider<ObservationRegistry> registryProvider) {
6878
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
6979
expressionHandler);
70-
defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults);
7180
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
72-
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
81+
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
82+
.preAuthorize(authorizationManager);
83+
return new DeferringMethodInterceptor<>(interceptor,
84+
(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
7385
}
7486

7587
@Bean
7688
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
77-
static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
78-
MethodSecurityExpressionHandler expressionHandler,
89+
static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurityExpressionHandler expressionHandler,
7990
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider) {
8091
PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(
8192
expressionHandler);
82-
defaultsObjectProvider.ifAvailable(interceptor::setTemplateDefaults);
83-
return interceptor;
93+
return new DeferringMethodInterceptor<>(interceptor,
94+
(i) -> defaultsObjectProvider.ifAvailable(i::setTemplateDefaults));
8495
}
8596

8697
@Bean
8798
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
88-
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
99+
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
89100
MethodSecurityExpressionHandler expressionHandler,
90101
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
91102
ObjectProvider<ObservationRegistry> registryProvider) {
92103
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
93104
expressionHandler);
94105
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
95-
defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults);
96-
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
106+
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
107+
.postAuthorize(authorizationManager);
108+
return new DeferringMethodInterceptor<>(interceptor,
109+
(i) -> defaultsObjectProvider.ifAvailable(manager::setTemplateDefaults));
97110
}
98111

99112
@Bean
@@ -112,4 +125,50 @@ static <T> ReactiveAuthorizationManager<T> manager(ReactiveAuthorizationManager<
112125
return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
113126
}
114127

128+
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
129+
implements AuthorizationAdvisor {
130+
131+
private final Pointcut pointcut;
132+
133+
private final int order;
134+
135+
private final Supplier<M> delegate;
136+
137+
DeferringMethodInterceptor(M delegate, Consumer<M> supplier) {
138+
this.pointcut = delegate.getPointcut();
139+
this.order = delegate.getOrder();
140+
this.delegate = SingletonSupplier.of(() -> {
141+
supplier.accept(delegate);
142+
return delegate;
143+
});
144+
}
145+
146+
@Nullable
147+
@Override
148+
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
149+
return this.delegate.get().invoke(invocation);
150+
}
151+
152+
@Override
153+
public Pointcut getPointcut() {
154+
return this.pointcut;
155+
}
156+
157+
@Override
158+
public Advice getAdvice() {
159+
return this;
160+
}
161+
162+
@Override
163+
public int getOrder() {
164+
return this.order;
165+
}
166+
167+
@Override
168+
public boolean isPerInstance() {
169+
return true;
170+
}
171+
172+
}
173+
115174
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.method.configuration;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.springframework.aop.framework.AopInfrastructureBean;
23+
import org.springframework.beans.factory.ObjectProvider;
24+
import org.springframework.beans.factory.config.BeanDefinition;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.context.annotation.Role;
28+
import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory;
29+
import org.springframework.security.authorization.method.AuthorizationAdvisor;
30+
31+
@Configuration(proxyBeanMethods = false)
32+
final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
33+
34+
@Bean
35+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
36+
static ReactiveAuthorizationAdvisorProxyFactory authorizationProxyFactory(
37+
ObjectProvider<AuthorizationAdvisor> provider) {
38+
List<AuthorizationAdvisor> advisors = new ArrayList<>();
39+
provider.forEach(advisors::add);
40+
ReactiveAuthorizationAdvisorProxyFactory factory = new ReactiveAuthorizationAdvisorProxyFactory();
41+
factory.setAdvisors(advisors);
42+
return factory;
43+
}
44+
45+
}

config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,15 @@ public String[] selectImports(AnnotationMetadata importMetadata) {
5151
else {
5252
imports.add(ReactiveMethodSecurityConfiguration.class.getName());
5353
}
54+
imports.add(ReactiveAuthorizationProxyConfiguration.class.getName());
5455
return imports.toArray(new String[0]);
5556
}
5657

5758
private static final class AutoProxyRegistrarSelector
5859
extends AdviceModeImportSelector<EnableReactiveMethodSecurity> {
5960

60-
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() };
61+
private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName(),
62+
MethodSecurityAdvisorRegistrar.class.getName() };
6163

6264
@Override
6365
protected String[] selectImports(@NonNull AdviceMode adviceMode) {

config/src/test/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfigurationTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@
1818

1919
import org.junit.jupiter.api.Test;
2020
import org.junit.jupiter.api.extension.ExtendWith;
21+
import reactor.core.publisher.Mono;
22+
import reactor.test.StepVerifier;
2123

2224
import org.springframework.beans.factory.annotation.Autowired;
2325
import org.springframework.context.annotation.Configuration;
2426
import org.springframework.security.access.AccessDeniedException;
2527
import org.springframework.security.access.prepost.PostAuthorize;
2628
import org.springframework.security.access.prepost.PreAuthorize;
29+
import org.springframework.security.authentication.TestAuthentication;
2730
import org.springframework.security.authorization.AuthorizationProxyFactory;
2831
import org.springframework.security.config.test.SpringTestContext;
2932
import org.springframework.security.config.test.SpringTestContextExtension;
33+
import org.springframework.security.core.Authentication;
34+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
3035
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
3136
import org.springframework.security.test.context.support.WithMockUser;
3237
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -69,12 +74,42 @@ public void proxyWhenPreAuthorizedThenAllows() {
6974
assertThat(toaster.extractBread()).isEqualTo("yummy");
7075
}
7176

77+
@Test
78+
public void proxyReactiveWhenNotPreAuthorizedThenDenies() {
79+
this.spring.register(ReactiveDefaultsConfig.class).autowire();
80+
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
81+
Authentication user = TestAuthentication.authenticatedUser();
82+
StepVerifier
83+
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
84+
.verifyError(AccessDeniedException.class);
85+
StepVerifier
86+
.create(toaster.reactiveExtractBread().contextWrite(ReactiveSecurityContextHolder.withAuthentication(user)))
87+
.verifyError(AccessDeniedException.class);
88+
}
89+
90+
@Test
91+
public void proxyReactiveWhenPreAuthorizedThenAllows() {
92+
this.spring.register(ReactiveDefaultsConfig.class).autowire();
93+
Toaster toaster = (Toaster) this.proxyFactory.proxy(new Toaster());
94+
Authentication admin = TestAuthentication.authenticatedAdmin();
95+
StepVerifier
96+
.create(toaster.reactiveMakeToast().contextWrite(ReactiveSecurityContextHolder.withAuthentication(admin)))
97+
.expectNext()
98+
.verifyComplete();
99+
}
100+
72101
@EnableMethodSecurity
73102
@Configuration
74103
static class DefaultsConfig {
75104

76105
}
77106

107+
@EnableReactiveMethodSecurity
108+
@Configuration
109+
static class ReactiveDefaultsConfig {
110+
111+
}
112+
78113
static class Toaster {
79114

80115
@PreAuthorize("hasRole('ADMIN')")
@@ -87,6 +122,16 @@ String extractBread() {
87122
return "yummy";
88123
}
89124

125+
@PreAuthorize("hasRole('ADMIN')")
126+
Mono<Void> reactiveMakeToast() {
127+
return Mono.empty();
128+
}
129+
130+
@PostAuthorize("hasRole('ADMIN')")
131+
Mono<String> reactiveExtractBread() {
132+
return Mono.just("yummy");
133+
}
134+
90135
}
91136

92137
}

0 commit comments

Comments
 (0)