Skip to content

Commit ada9549

Browse files
Publish Authorization Events on WebFlux
Closes gh-4961
1 parent c19f3d9 commit ada9549

File tree

18 files changed

+1169
-25
lines changed

18 files changed

+1169
-25
lines changed

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

+13-4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
2929
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
3030
import org.springframework.security.authentication.ReactiveAuthenticationManager;
31+
import org.springframework.security.authorization.ReactiveAuthorizationEventPublisher;
3132
import org.springframework.security.authorization.ReactiveAuthorizationManager;
3233
import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor;
3334
import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor;
@@ -57,10 +58,14 @@ static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
5758
@Bean
5859
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
5960
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
60-
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
61+
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider,
62+
ObjectProvider<ReactiveAuthorizationEventPublisher> eventPublisherProvider) {
6163
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(
6264
new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
63-
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
65+
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
66+
.preAuthorize(authorizationManager);
67+
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
68+
return interceptor;
6469
}
6570

6671
@Bean
@@ -73,10 +78,14 @@ static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
7378
@Bean
7479
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
7580
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
76-
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
81+
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider,
82+
ObjectProvider<ReactiveAuthorizationEventPublisher> eventPublisherProvider) {
7783
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(
7884
new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
79-
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
85+
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
86+
.postAuthorize(authorizationManager);
87+
eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher);
88+
return interceptor;
8089
}
8190

8291
@Bean

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

+18
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
5757
import org.springframework.security.authorization.AuthorizationDecision;
5858
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
59+
import org.springframework.security.authorization.ReactiveAuthorizationEventPublisher;
5960
import org.springframework.security.authorization.ReactiveAuthorizationManager;
61+
import org.springframework.security.authorization.SpringReactiveAuthorizationEventPublisher;
6062
import org.springframework.security.config.Customizer;
6163
import org.springframework.security.core.Authentication;
6264
import org.springframework.security.core.GrantedAuthority;
@@ -1794,6 +1796,8 @@ public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegis
17941796

17951797
private PathPatternParser pathPatternParser;
17961798

1799+
private ReactiveAuthorizationEventPublisher authorizationEventPublisher;
1800+
17971801
/**
17981802
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
17991803
* @return the {@link ServerHttpSecurity} to continue configuring
@@ -1851,9 +1855,23 @@ protected void configure(ServerHttpSecurity http) {
18511855
manager = new ObservationReactiveAuthorizationManager<>(registry, manager);
18521856
}
18531857
AuthorizationWebFilter result = new AuthorizationWebFilter(manager);
1858+
ReactiveAuthorizationEventPublisher authorizationEventPublisher = getAuthorizationEventPublisher(http);
1859+
if (authorizationEventPublisher != null) {
1860+
result.setAuthorizationEventPublisher(authorizationEventPublisher);
1861+
}
18541862
http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION);
18551863
}
18561864

1865+
private ReactiveAuthorizationEventPublisher getAuthorizationEventPublisher(ServerHttpSecurity http) {
1866+
if (this.authorizationEventPublisher == null) {
1867+
this.authorizationEventPublisher = getBeanOrNull(ReactiveAuthorizationEventPublisher.class);
1868+
}
1869+
if (this.authorizationEventPublisher == null && http.context != null) {
1870+
this.authorizationEventPublisher = new SpringReactiveAuthorizationEventPublisher(http.context);
1871+
}
1872+
return this.authorizationEventPublisher;
1873+
}
1874+
18571875
/**
18581876
* Configures the access for a particular set of exchanges.
18591877
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright 2002-2023 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.concurrent.ArrayBlockingQueue;
20+
import java.util.concurrent.BlockingQueue;
21+
import java.util.concurrent.TimeUnit;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import reactor.core.publisher.Mono;
26+
import reactor.test.StepVerifier;
27+
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.context.ApplicationEventPublisher;
30+
import org.springframework.context.ApplicationListener;
31+
import org.springframework.context.annotation.Bean;
32+
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.security.access.AccessDeniedException;
34+
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
35+
import org.springframework.security.authorization.AuthorizationDecision;
36+
import org.springframework.security.authorization.ReactiveAuthorizationEventPublisher;
37+
import org.springframework.security.authorization.event.ReactiveAuthorizationDeniedEvent;
38+
import org.springframework.security.authorization.event.ReactiveAuthorizationEvent;
39+
import org.springframework.security.authorization.event.ReactiveAuthorizationGrantedEvent;
40+
import org.springframework.security.config.test.SpringTestContext;
41+
import org.springframework.security.config.test.SpringTestContextExtension;
42+
import org.springframework.security.core.Authentication;
43+
import org.springframework.security.core.GrantedAuthority;
44+
import org.springframework.security.test.context.support.ReactorContextTestExecutionListener;
45+
import org.springframework.security.test.context.support.WithMockUser;
46+
import org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener;
47+
import org.springframework.test.context.TestExecutionListeners;
48+
import org.springframework.test.context.junit.jupiter.SpringExtension;
49+
50+
import static org.assertj.core.api.Assertions.assertThat;
51+
52+
@ExtendWith({ SpringExtension.class, SpringTestContextExtension.class })
53+
@TestExecutionListeners(
54+
listeners = { WithSecurityContextTestExecutionListener.class, ReactorContextTestExecutionListener.class })
55+
public class ReactiveAuthorizationManagerMethodSecurityConfigurationTests {
56+
57+
public final SpringTestContext spring = new SpringTestContext(this);
58+
59+
@Autowired
60+
ReactiveMessageService messageService;
61+
62+
@Autowired
63+
ReactiveAuthorizationEventPublisher eventPublisher;
64+
65+
@Autowired
66+
MyEventListener eventListener;
67+
68+
AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
69+
70+
@Test
71+
void preAuthorizeMonoWhenDeniedThenPublishEvent() {
72+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
73+
StepVerifier.create(this.messageService.monoPreAuthorizeHasRoleFindById(1))
74+
.expectError(AccessDeniedException.class)
75+
.verify();
76+
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
77+
assertThat(event).isNotNull();
78+
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
79+
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
80+
}
81+
82+
@Test
83+
@WithMockUser(roles = "ADMIN")
84+
void preAuthorizeMonoWhenGrantedThenPublishEvent() {
85+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
86+
StepVerifier.create(this.messageService.monoPreAuthorizeHasRoleFindById(1)).verifyComplete();
87+
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
88+
assertThat(event).isNotNull();
89+
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
90+
StepVerifier.create(event.getAuthentication())
91+
.assertNext((auth) -> assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority)
92+
.contains("ROLE_ADMIN"))
93+
.verifyComplete();
94+
}
95+
96+
@Test
97+
void preAuthorizeFluxWhenDeniedThenPublishEvent() {
98+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
99+
StepVerifier.create(this.messageService.fluxPreAuthorizeHasRoleFindById(1))
100+
.expectError(AccessDeniedException.class)
101+
.verify();
102+
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
103+
assertThat(event).isNotNull();
104+
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
105+
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
106+
}
107+
108+
@Test
109+
@WithMockUser(roles = "ADMIN")
110+
void preAuthorizeFluxWhenGrantedThenPublishEvent() {
111+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
112+
StepVerifier.create(this.messageService.fluxPreAuthorizeHasRoleFindById(1)).verifyComplete();
113+
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
114+
assertThat(event).isNotNull();
115+
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
116+
StepVerifier.create(event.getAuthentication())
117+
.assertNext((auth) -> assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority)
118+
.contains("ROLE_ADMIN"))
119+
.verifyComplete();
120+
}
121+
122+
@Test
123+
void postAuthorizeMonoWhenDeniedThenPublishEvent() {
124+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
125+
StepVerifier.create(this.messageService.monoPostAuthorizeFindById(1))
126+
.expectError(AccessDeniedException.class)
127+
.verify();
128+
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
129+
assertThat(event).isNotNull();
130+
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
131+
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
132+
}
133+
134+
@Test
135+
@WithMockUser(roles = "ADMIN")
136+
void postAuthorizeMonoWhenGrantedThenPublishEvent() {
137+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
138+
StepVerifier.create(this.messageService.monoPostAuthorizeFindById(1)).expectNext("user").verifyComplete();
139+
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
140+
assertThat(event).isNotNull();
141+
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
142+
StepVerifier.create(event.getAuthentication())
143+
.assertNext((auth) -> assertThat(auth.getAuthorities()).extracting(GrantedAuthority::getAuthority)
144+
.contains("ROLE_ADMIN"))
145+
.verifyComplete();
146+
}
147+
148+
@Test
149+
@WithMockUser(username = "notuser")
150+
void postAuthorizeFluxWhenDeniedThenPublishEvent() {
151+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
152+
StepVerifier.create(this.messageService.fluxPostAuthorizeFindById(1))
153+
.expectError(AccessDeniedException.class)
154+
.verify();
155+
ReactiveAuthorizationDeniedEvent<?> event = this.eventListener.getEvent();
156+
assertThat(event).isNotNull();
157+
assertThat(event.getAuthorizationDecision().isGranted()).isFalse();
158+
StepVerifier.create(event.getAuthentication()).assertNext(this.trustResolver::isAnonymous).verifyComplete();
159+
}
160+
161+
@Test
162+
@WithMockUser
163+
void postAuthorizeFluxWhenGrantedThenPublishEvent() {
164+
this.spring.register(Config.class, AuthorizationEventPublisherConfig.class).autowire();
165+
StepVerifier.create(this.messageService.fluxPostAuthorizeFindById(1)).expectNext("user").verifyComplete();
166+
ReactiveAuthorizationGrantedEvent<?> event = this.eventListener.getEvent();
167+
assertThat(event).isNotNull();
168+
assertThat(event.getAuthorizationDecision().isGranted()).isTrue();
169+
StepVerifier.create(event.getAuthentication()).expectNextCount(1).verifyComplete();
170+
}
171+
172+
@Configuration
173+
@EnableReactiveMethodSecurity
174+
static class Config {
175+
176+
@Bean
177+
DelegatingReactiveMessageService defaultMessageService() {
178+
return new DelegatingReactiveMessageService(new StubReactiveMessageService());
179+
}
180+
181+
@Bean
182+
Authz authz() {
183+
return new Authz();
184+
}
185+
186+
}
187+
188+
@Configuration
189+
static class AuthorizationEventPublisherConfig {
190+
191+
@Bean
192+
ReactiveAuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher eventPublisher) {
193+
return new ReactiveAuthorizationEventPublisher() {
194+
@Override
195+
public <T> void publishAuthorizationEvent(Mono<Authentication> authentication, T object,
196+
AuthorizationDecision decision) {
197+
ReactiveAuthorizationEvent event;
198+
if (decision.isGranted()) {
199+
event = new ReactiveAuthorizationGrantedEvent<>(authentication, object, decision);
200+
}
201+
else {
202+
event = new ReactiveAuthorizationDeniedEvent<>(authentication, object, decision);
203+
}
204+
eventPublisher.publishEvent(event);
205+
}
206+
};
207+
}
208+
209+
@Bean
210+
MyEventListener myEventListener() {
211+
return new MyEventListener();
212+
}
213+
214+
}
215+
216+
public static class MyEventListener implements ApplicationListener<ReactiveAuthorizationEvent> {
217+
218+
static BlockingQueue<ReactiveAuthorizationEvent> events = new ArrayBlockingQueue<>(10);
219+
220+
public <T extends ReactiveAuthorizationEvent> T getEvent() {
221+
try {
222+
return (T) events.poll(1, TimeUnit.SECONDS);
223+
}
224+
catch (InterruptedException ex) {
225+
return null;
226+
}
227+
}
228+
229+
@Override
230+
public void onApplicationEvent(ReactiveAuthorizationEvent event) {
231+
events.add(event);
232+
}
233+
234+
}
235+
236+
}

0 commit comments

Comments
 (0)