Skip to content

Commit 9458473

Browse files
Add Reactive Support
1 parent 526d87d commit 9458473

8 files changed

+418
-30
lines changed

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.beans.factory.ObjectProvider;
3232
import org.springframework.beans.factory.annotation.Autowired;
3333
import org.springframework.beans.factory.config.BeanDefinition;
34+
import org.springframework.context.ApplicationContext;
3435
import org.springframework.context.annotation.Bean;
3536
import org.springframework.context.annotation.Configuration;
3637
import org.springframework.context.annotation.Role;
@@ -74,9 +75,10 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityE
7475
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
7576
MethodSecurityExpressionHandler expressionHandler,
7677
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
77-
ObjectProvider<ObservationRegistry> registryProvider) {
78+
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
7879
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
7980
expressionHandler);
81+
manager.setApplicationContext(context);
8082
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
8183
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
8284
.preAuthorize(authorizationManager);
@@ -99,9 +101,10 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurity
99101
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
100102
MethodSecurityExpressionHandler expressionHandler,
101103
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
102-
ObjectProvider<ObservationRegistry> registryProvider) {
104+
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
103105
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
104106
expressionHandler);
107+
manager.setApplicationContext(context);
105108
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
106109
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
107110
.postAuthorize(authorizationManager);

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

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121

2222
public interface AuthorizationDeniedPostProcessor<T> {
2323

24+
/**
25+
* @param contextObject the object containing context information for an authorization
26+
* decision
27+
* @param result the {@link AuthorizationResult} containing the authorization details
28+
* @return the object to be returned to the component that is not authorized, can also
29+
* be an instance of {@link reactor.core.publisher.Mono}.
30+
*/
2431
@Nullable
2532
Object postProcessResult(T contextObject, AuthorizationResult result);
2633

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

+37-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
import org.springframework.core.ReactiveAdapter;
3434
import org.springframework.core.ReactiveAdapterRegistry;
3535
import org.springframework.security.access.prepost.PostAuthorize;
36+
import org.springframework.security.authorization.AuthorizationDecision;
37+
import org.springframework.security.authorization.AuthorizationDeniedException;
38+
import org.springframework.security.authorization.AuthorizationResult;
3639
import org.springframework.security.authorization.ReactiveAuthorizationManager;
3740
import org.springframework.security.core.Authentication;
3841
import org.springframework.util.Assert;
@@ -57,6 +60,8 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
5760

5861
private int order = AuthorizationInterceptorsOrder.LAST.getOrder();
5962

63+
private final AuthorizationDeniedPostProcessor<MethodInvocationResult> postProcessor = new ReactiveAuthorizationDeniedPostProcessorAdapter();
64+
6065
/**
6166
* Creates an instance for the {@link PostAuthorize} annotation.
6267
* @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use
@@ -144,9 +149,28 @@ private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
144149
return adapter != null && adapter.isMultiValue();
145150
}
146151

147-
private Mono<?> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
148-
return this.authorizationManager.verify(authentication, new MethodInvocationResult(mi, result))
149-
.thenReturn(result);
152+
private Mono<Object> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
153+
MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result);
154+
return this.authorizationManager.check(authentication, invocationResult)
155+
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
156+
.flatMap((decision) -> postProcess(decision, invocationResult));
157+
}
158+
159+
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) {
160+
if (decision.isGranted()) {
161+
return Mono.just(methodInvocationResult.getResult());
162+
}
163+
return Mono.fromSupplier(() -> {
164+
if (decision instanceof PostProcessableAuthorizationDecision<?> postProcessableDecision) {
165+
return postProcessableDecision.postProcess();
166+
}
167+
return this.postProcessor.postProcessResult(methodInvocationResult, decision);
168+
}).flatMap((processedResult) -> {
169+
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
170+
return (Mono<?>) processedResult;
171+
}
172+
return Mono.justOrEmpty(processedResult);
173+
});
150174
}
151175

152176
@Override
@@ -184,4 +208,14 @@ private static Object asFlow(Publisher<?> publisher) {
184208

185209
}
186210

211+
private static class ReactiveAuthorizationDeniedPostProcessorAdapter
212+
implements AuthorizationDeniedPostProcessor<MethodInvocationResult> {
213+
214+
@Override
215+
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
216+
return Mono.error(new AuthorizationDeniedException("Access Denied", result));
217+
}
218+
219+
}
220+
187221
}

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

+58-9
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
import org.springframework.core.ReactiveAdapter;
3333
import org.springframework.core.ReactiveAdapterRegistry;
3434
import org.springframework.security.access.prepost.PreAuthorize;
35+
import org.springframework.security.authorization.AuthorizationDecision;
36+
import org.springframework.security.authorization.AuthorizationDeniedException;
37+
import org.springframework.security.authorization.AuthorizationResult;
3538
import org.springframework.security.authorization.ReactiveAuthorizationManager;
3639
import org.springframework.security.core.Authentication;
3740
import org.springframework.util.Assert;
@@ -57,6 +60,8 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
5760

5861
private int order = AuthorizationInterceptorsOrder.FIRST.getOrder();
5962

63+
private final AuthorizationDeniedPostProcessor<MethodInvocation> postProcessor = new ReactiveAuthorizationDeniedPostProcessorAdapter();
64+
6065
/**
6166
* Creates an instance for the {@link PreAuthorize} annotation.
6267
* @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use
@@ -112,31 +117,65 @@ public Object invoke(MethodInvocation mi) throws Throwable {
112117
+ " must return an instance of org.reactivestreams.Publisher "
113118
+ "(for example, a Mono or Flux) or the function must be a Kotlin coroutine "
114119
+ "in order to support Reactor Context");
115-
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
116120
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
117-
Mono<Void> preAuthorize = this.authorizationManager.verify(authentication, mi);
118121
if (hasFlowReturnType) {
119122
if (isSuspendingFunction) {
120-
return preAuthorize.thenMany(Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
123+
return preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
121124
}
122125
else {
123126
Assert.state(adapter != null, () -> "The returnType " + type + " on " + method
124127
+ " must have a org.springframework.core.ReactiveAdapter registered");
125-
Flux<?> response = preAuthorize
126-
.thenMany(Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
128+
Flux<Object> response = preAuthorized(mi,
129+
Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
127130
return KotlinDelegate.asFlow(response);
128131
}
129132
}
130133
if (isMultiValue(type, adapter)) {
131-
Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
132-
Flux<?> result = preAuthorize.thenMany(publisher);
134+
Flux<?> result = preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
133135
return (adapter != null) ? adapter.fromPublisher(result) : result;
134136
}
135-
Mono<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
136-
Mono<?> result = preAuthorize.then(publisher);
137+
Mono<?> result = preAuthorized(mi, Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
137138
return (adapter != null) ? adapter.fromPublisher(result) : result;
138139
}
139140

141+
private Flux<Object> preAuthorized(MethodInvocation mi, Flux<Object> mapping) {
142+
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
143+
return this.authorizationManager.check(authentication, mi)
144+
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
145+
.flatMapMany((decision) -> {
146+
if (decision.isGranted()) {
147+
return mapping;
148+
}
149+
return postProcess(decision, mi);
150+
});
151+
}
152+
153+
private Mono<Object> preAuthorized(MethodInvocation mi, Mono<Object> mapping) {
154+
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
155+
return this.authorizationManager.check(authentication, mi)
156+
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
157+
.flatMap((decision) -> {
158+
if (decision.isGranted()) {
159+
return mapping;
160+
}
161+
return postProcess(decision, mi);
162+
});
163+
}
164+
165+
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
166+
return Mono.fromSupplier(() -> {
167+
if (decision instanceof PostProcessableAuthorizationDecision<?> postProcessableDecision) {
168+
return postProcessableDecision.postProcess();
169+
}
170+
return this.postProcessor.postProcessResult(mi, decision);
171+
}).flatMap((result) -> {
172+
if (Mono.class.isAssignableFrom(result.getClass())) {
173+
return (Mono<?>) result;
174+
}
175+
return Mono.justOrEmpty(result);
176+
});
177+
}
178+
140179
private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
141180
if (Flux.class.isAssignableFrom(returnType)) {
142181
return true;
@@ -179,4 +218,14 @@ private static Object asFlow(Publisher<?> publisher) {
179218

180219
}
181220

221+
private static class ReactiveAuthorizationDeniedPostProcessorAdapter
222+
implements AuthorizationDeniedPostProcessor<MethodInvocation> {
223+
224+
@Override
225+
public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) {
226+
return Mono.error(new AuthorizationDeniedException("Access Denied", result));
227+
}
228+
229+
}
230+
182231
}

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.aopalliance.intercept.MethodInvocation;
2020
import reactor.core.publisher.Mono;
2121

22+
import org.springframework.context.ApplicationContext;
2223
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
2324
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
2425
import org.springframework.security.access.prepost.PostAuthorize;
@@ -61,6 +62,10 @@ public void setTemplateDefaults(PrePostTemplateDefaults defaults) {
6162
this.registry.setTemplateDefaults(defaults);
6263
}
6364

65+
public void setApplicationContext(ApplicationContext context) {
66+
this.registry.setApplicationContext(context);
67+
}
68+
6469
/**
6570
* Determines if an {@link Authentication} has access to the returned object from the
6671
* {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize}
@@ -77,13 +82,14 @@ public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, Me
7782
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
7883
return Mono.empty();
7984
}
85+
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
8086
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
8187
// @formatter:off
8288
return authentication
8389
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
8490
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
8591
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
86-
.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
92+
.map((granted) -> new PostProcessableAuthorizationDecision<>(granted, postAuthorizeAttribute.getExpression(), result, postAuthorizeAttribute.getPostProcessor()));
8793
// @formatter:on
8894
}
8995

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.aopalliance.intercept.MethodInvocation;
2020
import reactor.core.publisher.Mono;
2121

22+
import org.springframework.context.ApplicationContext;
2223
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
2324
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
2425
import org.springframework.security.access.prepost.PreAuthorize;
@@ -60,6 +61,10 @@ public void setTemplateDefaults(PrePostTemplateDefaults defaults) {
6061
this.registry.setTemplateDefaults(defaults);
6162
}
6263

64+
public void setApplicationContext(ApplicationContext context) {
65+
this.registry.setApplicationContext(context);
66+
}
67+
6368
/**
6469
* Determines if an {@link Authentication} has access to the {@link MethodInvocation}
6570
* by evaluating an expression from the {@link PreAuthorize} annotation.
@@ -74,11 +79,12 @@ public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, Me
7479
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
7580
return Mono.empty();
7681
}
82+
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
7783
// @formatter:off
7884
return authentication
7985
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
8086
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
81-
.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
87+
.map((granted) -> new PostProcessableAuthorizationDecision<>(granted, preAuthorizeAttribute.getExpression(), mi, preAuthorizeAttribute.getPostProcessor()));
8288
// @formatter:on
8389
}
8490

0 commit comments

Comments
 (0)