Skip to content

Commit 9751672

Browse files
committed
Add Coroutine Support
Closes gh-12080
1 parent cad6689 commit 9751672

File tree

7 files changed

+214
-24
lines changed

7 files changed

+214
-24
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -80,7 +80,7 @@ public void notPublisherPreAuthorizeFindByIdThenThrowsIllegalStateException() {
8080
.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
8181
+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
8282
+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
83-
+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
83+
+ ".Publisher (for example, a Mono or Flux) or the function must be a Kotlin coroutine in order to support Reactor Context");
8484
}
8585

8686
@Test

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public void notPublisherPreAuthorizeFindByIdThenThrowsIllegalStateException() {
7878
.withMessage("The returnType class java.lang.String on public abstract java.lang.String "
7979
+ "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService"
8080
+ ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams"
81-
+ ".Publisher (for example, a Mono or Flux) in order to support Reactor Context");
81+
+ ".Publisher (for example, a Mono or Flux) or the function must be a Kotlin coroutine in order to support Reactor Context");
8282
}
8383

8484
@Test
Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension
4141

4242
@ExtendWith(SpringExtension::class)
4343
@ContextConfiguration
44-
// no authorization manager due to https://github.com/spring-projects/spring-security/issues/12080
45-
class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests {
44+
class KotlinEnableReactiveMethodSecurityTests {
4645

4746
private lateinit var delegate: KotlinReactiveMessageService
4847

@@ -138,6 +137,39 @@ class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests {
138137
coVerify(exactly = 1) { delegate.suspendingPreAuthorizeHasRole() }
139138
}
140139

140+
@Test
141+
@WithMockUser
142+
fun `suspendingPrePostAuthorizeHasRoleContainsName when not pre authorized then delegate not called`() {
143+
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
144+
runBlocking {
145+
messageService!!.suspendingPrePostAuthorizeHasRoleContainsName()
146+
}
147+
}
148+
verify { delegate wasNot Called }
149+
}
150+
151+
@Test
152+
@WithMockUser(authorities = ["ROLE_ADMIN"])
153+
fun `suspendingPrePostAuthorizeHasRoleContainsName when not post authorized then exception`() {
154+
coEvery { delegate.suspendingPrePostAuthorizeHasRoleContainsName() } returns "wrong"
155+
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
156+
runBlocking {
157+
messageService!!.suspendingPrePostAuthorizeHasRoleContainsName()
158+
}
159+
}
160+
coVerify(exactly = 1) { delegate.suspendingPrePostAuthorizeHasRoleContainsName() }
161+
}
162+
163+
@Test
164+
@WithMockUser(authorities = ["ROLE_ADMIN"])
165+
fun `suspendingPrePostAuthorizeHasRoleContainsName when authorized then success`() {
166+
coEvery { delegate.suspendingPrePostAuthorizeHasRoleContainsName() } returns "user"
167+
runBlocking {
168+
assertThat(messageService!!.suspendingPrePostAuthorizeHasRoleContainsName()).contains("user")
169+
}
170+
coVerify(exactly = 1) { delegate.suspendingPrePostAuthorizeHasRoleContainsName() }
171+
}
172+
141173
@Test
142174
@WithMockUser(authorities = ["ROLE_ADMIN"])
143175
fun `suspendingFlowPreAuthorize when user has role then success`() {
@@ -181,6 +213,33 @@ class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests {
181213
verify { delegate wasNot Called }
182214
}
183215

216+
@Test
217+
fun `suspendingFlowPrePostAuthorizeBean when not pre authorized then delegate not called`() {
218+
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
219+
runBlocking {
220+
messageService!!.suspendingFlowPrePostAuthorizeBean(true).collect()
221+
}
222+
}
223+
}
224+
225+
@Test
226+
@WithMockUser(roles = ["ADMIN"])
227+
fun `suspendingFlowPrePostAuthorizeBean when not post authorized then denied`() {
228+
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
229+
runBlocking {
230+
messageService!!.suspendingFlowPrePostAuthorizeBean(false).collect()
231+
}
232+
}
233+
}
234+
235+
@Test
236+
@WithMockUser(roles = ["ADMIN"])
237+
fun `suspendingFlowPrePostAuthorizeBean when authorized then success`() {
238+
runBlocking {
239+
assertThat(messageService!!.suspendingFlowPrePostAuthorizeBean(true).toList()).containsExactly(1, 2, 3)
240+
}
241+
}
242+
184243
@Test
185244
@WithMockUser(authorities = ["ROLE_ADMIN"])
186245
fun `suspendingFlowPreAuthorizeDelegate when user has role then delegate called`() {
@@ -244,8 +303,35 @@ class KotlinEnableReactiveMethodSecurityNoAuthorizationManagerTests {
244303
coVerify(exactly = 1) { delegate.flowPreAuthorize() }
245304
}
246305

306+
@Test
307+
fun `flowPrePostAuthorize when not pre authorized then denied`() {
308+
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
309+
runBlocking {
310+
messageService!!.flowPrePostAuthorize(true).collect()
311+
}
312+
}
313+
}
314+
315+
@Test
316+
@WithMockUser(roles = ["ADMIN"])
317+
fun `flowPrePostAuthorize when not post authorized then denied`() {
318+
assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
319+
runBlocking {
320+
messageService!!.flowPrePostAuthorize(false).collect()
321+
}
322+
}
323+
}
324+
325+
@Test
326+
@WithMockUser(roles = ["ADMIN"])
327+
fun `flowPrePostAuthorize when authorized then success`() {
328+
runBlocking {
329+
assertThat(messageService!!.flowPrePostAuthorize(true).toList()).containsExactly(1, 2, 3)
330+
}
331+
}
332+
247333
@Configuration
248-
@EnableReactiveMethodSecurity(useAuthorizationManager = false)
334+
@EnableReactiveMethodSecurity
249335
open class Config {
250336
var delegate = mockk<KotlinReactiveMessageService>()
251337

config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageService.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,15 +30,21 @@ interface KotlinReactiveMessageService {
3030

3131
suspend fun suspendingPreAuthorizeDelegate(): String
3232

33+
suspend fun suspendingPrePostAuthorizeHasRoleContainsName(): String
34+
3335
suspend fun suspendingFlowPreAuthorize(): Flow<Int>
3436

3537
suspend fun suspendingFlowPostAuthorize(id: Boolean): Flow<Int>
3638

3739
suspend fun suspendingFlowPreAuthorizeDelegate(): Flow<Int>
3840

41+
suspend fun suspendingFlowPrePostAuthorizeBean(id: Boolean): Flow<Int>
42+
3943
fun flowPreAuthorize(): Flow<Int>
4044

4145
fun flowPostAuthorize(id: Boolean): Flow<Int>
4246

4347
fun flowPreAuthorizeDelegate(): Flow<Int>
48+
49+
fun flowPrePostAuthorize(id: Boolean): Flow<Int>
4450
}

config/src/test/kotlin/org/springframework/security/config/annotation/method/configuration/KotlinReactiveMessageServiceImpl.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -47,6 +47,12 @@ class KotlinReactiveMessageServiceImpl(val delegate: KotlinReactiveMessageServic
4747
return "user"
4848
}
4949

50+
@PreAuthorize("hasRole('ADMIN')")
51+
@PostAuthorize("returnObject?.contains(authentication?.name)")
52+
override suspend fun suspendingPrePostAuthorizeHasRoleContainsName(): String {
53+
return delegate.suspendingPrePostAuthorizeHasRoleContainsName()
54+
}
55+
5056
@PreAuthorize("hasRole('ADMIN')")
5157
override suspend fun suspendingPreAuthorizeDelegate(): String {
5258
return delegate.suspendingPreAuthorizeHasRole()
@@ -80,6 +86,18 @@ class KotlinReactiveMessageServiceImpl(val delegate: KotlinReactiveMessageServic
8086
return delegate.flowPreAuthorize()
8187
}
8288

89+
@PreAuthorize("hasRole('ADMIN')")
90+
@PostAuthorize("@authz.check(#id)")
91+
override suspend fun suspendingFlowPrePostAuthorizeBean(id: Boolean): Flow<Int> {
92+
delay(1)
93+
return flow {
94+
for (i in 1..3) {
95+
delay(1)
96+
emit(i)
97+
}
98+
}
99+
}
100+
83101
@PreAuthorize("hasRole('ADMIN')")
84102
override fun flowPreAuthorize(): Flow<Int> {
85103
return flow {
@@ -104,4 +122,15 @@ class KotlinReactiveMessageServiceImpl(val delegate: KotlinReactiveMessageServic
104122
override fun flowPreAuthorizeDelegate(): Flow<Int> {
105123
return delegate.flowPreAuthorize()
106124
}
125+
126+
@PreAuthorize("hasRole('ADMIN')")
127+
@PostAuthorize("@authz.check(#id)")
128+
override fun flowPrePostAuthorize(id: Boolean): Flow<Int> {
129+
return flow {
130+
for (i in 1..3) {
131+
delay(1)
132+
emit(i)
133+
}
134+
}
135+
}
107136
}

core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Method;
2020
import java.util.function.Function;
2121

22+
import kotlinx.coroutines.reactive.ReactiveFlowKt;
2223
import org.aopalliance.aop.Advice;
2324
import org.aopalliance.intercept.MethodInterceptor;
2425
import org.aopalliance.intercept.MethodInvocation;
@@ -29,6 +30,8 @@
2930
import org.springframework.aop.Pointcut;
3031
import org.springframework.aop.PointcutAdvisor;
3132
import org.springframework.aop.framework.AopInfrastructureBean;
33+
import org.springframework.core.KotlinDetector;
34+
import org.springframework.core.MethodParameter;
3235
import org.springframework.core.Ordered;
3336
import org.springframework.core.ReactiveAdapter;
3437
import org.springframework.core.ReactiveAdapterRegistry;
@@ -48,6 +51,10 @@
4851
public final class AuthorizationManagerAfterReactiveMethodInterceptor
4952
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
5053

54+
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
55+
56+
private static final int RETURN_TYPE_METHOD_PARAMETER_INDEX = -1;
57+
5158
private final Pointcut pointcut;
5259

5360
private final ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager;
@@ -99,15 +106,32 @@ public AuthorizationManagerAfterReactiveMethodInterceptor(Pointcut pointcut,
99106
public Object invoke(MethodInvocation mi) throws Throwable {
100107
Method method = mi.getMethod();
101108
Class<?> type = method.getReturnType();
102-
Assert
103-
.state(Publisher.class.isAssignableFrom(type),
104-
() -> String.format(
105-
"The returnType %s on %s must return an instance of org.reactivestreams.Publisher "
106-
+ "(for example, a Mono or Flux) in order to support Reactor Context",
107-
type, method));
109+
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
110+
boolean hasFlowReturnType = COROUTINES_FLOW_CLASS_NAME
111+
.equals(new MethodParameter(method, RETURN_TYPE_METHOD_PARAMETER_INDEX).getParameterType().getName());
112+
boolean hasReactiveReturnType = Publisher.class.isAssignableFrom(type) || isSuspendingFunction
113+
|| hasFlowReturnType;
114+
Assert.state(hasReactiveReturnType,
115+
() -> "The returnType " + type + " on " + method
116+
+ " must return an instance of org.reactivestreams.Publisher "
117+
+ "(for example, a Mono or Flux) or the function must be a Kotlin coroutine "
118+
+ "in order to support Reactor Context");
108119
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
109120
Function<Object, Mono<?>> postAuthorize = (result) -> postAuthorize(authentication, mi, result);
110121
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
122+
if (hasFlowReturnType) {
123+
if (isSuspendingFunction) {
124+
Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
125+
return Flux.from(publisher).flatMap(postAuthorize);
126+
}
127+
else {
128+
Assert.state(adapter != null, () -> "The returnType " + type + " on " + method
129+
+ " must have a org.springframework.core.ReactiveAdapter registered");
130+
Flux<?> response = Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi)))
131+
.flatMap(postAuthorize);
132+
return KotlinDelegate.asFlow(response);
133+
}
134+
}
111135
Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
112136
if (isMultiValue(type, adapter)) {
113137
Flux<?> flux = Flux.from(publisher).flatMap(postAuthorize);
@@ -121,7 +145,7 @@ private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
121145
if (Flux.class.isAssignableFrom(returnType)) {
122146
return true;
123147
}
124-
return adapter == null || adapter.isMultiValue();
148+
return adapter != null && adapter.isMultiValue();
125149
}
126150

127151
private Mono<?> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
@@ -153,4 +177,15 @@ public void setOrder(int order) {
153177
this.order = order;
154178
}
155179

180+
/**
181+
* Inner class to avoid a hard dependency on Kotlin at runtime.
182+
*/
183+
private static class KotlinDelegate {
184+
185+
private static Object asFlow(Publisher<?> publisher) {
186+
return ReactiveFlowKt.asFlow(publisher);
187+
}
188+
189+
}
190+
156191
}

0 commit comments

Comments
 (0)