Skip to content

Commit

Permalink
Add Reactive Support
Browse files Browse the repository at this point in the history
  • Loading branch information
marcusdacoregio committed Mar 26, 2024
1 parent 526d87d commit 9458473
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
Expand Down Expand Up @@ -74,9 +75,10 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityE
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
.preAuthorize(authorizationManager);
Expand All @@ -99,9 +101,10 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurity
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
.postAuthorize(authorizationManager);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@

public interface AuthorizationDeniedPostProcessor<T> {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
Expand All @@ -57,6 +60,8 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements

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

private final AuthorizationDeniedPostProcessor<MethodInvocationResult> postProcessor = new ReactiveAuthorizationDeniedPostProcessorAdapter();

/**
* Creates an instance for the {@link PostAuthorize} annotation.
* @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use
Expand Down Expand Up @@ -144,9 +149,28 @@ private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
return adapter != null && adapter.isMultiValue();
}

private Mono<?> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
return this.authorizationManager.verify(authentication, new MethodInvocationResult(mi, result))
.thenReturn(result);
private Mono<Object> postAuthorize(Mono<Authentication> authentication, MethodInvocation mi, Object result) {
MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result);
return this.authorizationManager.check(authentication, invocationResult)
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
.flatMap((decision) -> postProcess(decision, invocationResult));
}

private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) {
if (decision.isGranted()) {
return Mono.just(methodInvocationResult.getResult());
}
return Mono.fromSupplier(() -> {
if (decision instanceof PostProcessableAuthorizationDecision<?> postProcessableDecision) {
return postProcessableDecision.postProcess();
}
return this.postProcessor.postProcessResult(methodInvocationResult, decision);
}).flatMap((processedResult) -> {
if (Mono.class.isAssignableFrom(processedResult.getClass())) {
return (Mono<?>) processedResult;
}
return Mono.justOrEmpty(processedResult);
});
}

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

}

private static class ReactiveAuthorizationDeniedPostProcessorAdapter
implements AuthorizationDeniedPostProcessor<MethodInvocationResult> {

@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return Mono.error(new AuthorizationDeniedException("Access Denied", result));
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
Expand All @@ -57,6 +60,8 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement

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

private final AuthorizationDeniedPostProcessor<MethodInvocation> postProcessor = new ReactiveAuthorizationDeniedPostProcessorAdapter();

/**
* Creates an instance for the {@link PreAuthorize} annotation.
* @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use
Expand Down Expand Up @@ -112,31 +117,65 @@ public Object invoke(MethodInvocation mi) throws Throwable {
+ " must return an instance of org.reactivestreams.Publisher "
+ "(for example, a Mono or Flux) or the function must be a Kotlin coroutine "
+ "in order to support Reactor Context");
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
Mono<Void> preAuthorize = this.authorizationManager.verify(authentication, mi);
if (hasFlowReturnType) {
if (isSuspendingFunction) {
return preAuthorize.thenMany(Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
return preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
}
else {
Assert.state(adapter != null, () -> "The returnType " + type + " on " + method
+ " must have a org.springframework.core.ReactiveAdapter registered");
Flux<?> response = preAuthorize
.thenMany(Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
Flux<Object> response = preAuthorized(mi,
Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi))));
return KotlinDelegate.asFlow(response);
}
}
if (isMultiValue(type, adapter)) {
Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Flux<?> result = preAuthorize.thenMany(publisher);
Flux<?> result = preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
return (adapter != null) ? adapter.fromPublisher(result) : result;
}
Mono<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Mono<?> result = preAuthorize.then(publisher);
Mono<?> result = preAuthorized(mi, Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)));
return (adapter != null) ? adapter.fromPublisher(result) : result;
}

private Flux<Object> preAuthorized(MethodInvocation mi, Flux<Object> mapping) {
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
return this.authorizationManager.check(authentication, mi)
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
.flatMapMany((decision) -> {
if (decision.isGranted()) {
return mapping;
}
return postProcess(decision, mi);
});
}

private Mono<Object> preAuthorized(MethodInvocation mi, Mono<Object> mapping) {
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
return this.authorizationManager.check(authentication, mi)
.switchIfEmpty(Mono.just(new AuthorizationDecision(false)))
.flatMap((decision) -> {
if (decision.isGranted()) {
return mapping;
}
return postProcess(decision, mi);
});
}

private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
return Mono.fromSupplier(() -> {
if (decision instanceof PostProcessableAuthorizationDecision<?> postProcessableDecision) {
return postProcessableDecision.postProcess();
}
return this.postProcessor.postProcessResult(mi, decision);
}).flatMap((result) -> {
if (Mono.class.isAssignableFrom(result.getClass())) {
return (Mono<?>) result;
}
return Mono.justOrEmpty(result);
});
}

private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
if (Flux.class.isAssignableFrom(returnType)) {
return true;
Expand Down Expand Up @@ -179,4 +218,14 @@ private static Object asFlow(Publisher<?> publisher) {

}

private static class ReactiveAuthorizationDeniedPostProcessorAdapter
implements AuthorizationDeniedPostProcessor<MethodInvocation> {

@Override
public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) {
return Mono.error(new AuthorizationDeniedException("Access Denied", result));
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;

import org.springframework.context.ApplicationContext;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
Expand Down Expand Up @@ -61,6 +62,10 @@ public void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.registry.setTemplateDefaults(defaults);
}

public void setApplicationContext(ApplicationContext context) {
this.registry.setApplicationContext(context);
}

/**
* Determines if an {@link Authentication} has access to the returned object from the
* {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize}
Expand All @@ -77,13 +82,14 @@ public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, Me
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return Mono.empty();
}
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
// @formatter:off
return authentication
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
.map((granted) -> new PostProcessableAuthorizationDecision<>(granted, postAuthorizeAttribute.getExpression(), result, postAuthorizeAttribute.getPostProcessor()));
// @formatter:on
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;

import org.springframework.context.ApplicationContext;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PreAuthorize;
Expand Down Expand Up @@ -60,6 +61,10 @@ public void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.registry.setTemplateDefaults(defaults);
}

public void setApplicationContext(ApplicationContext context) {
this.registry.setApplicationContext(context);
}

/**
* Determines if an {@link Authentication} has access to the {@link MethodInvocation}
* by evaluating an expression from the {@link PreAuthorize} annotation.
Expand All @@ -74,11 +79,12 @@ public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, Me
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return Mono.empty();
}
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
// @formatter:off
return authentication
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx))
.map((granted) -> new ExpressionAttributeAuthorizationDecision(granted, attribute));
.map((granted) -> new PostProcessableAuthorizationDecision<>(granted, preAuthorizeAttribute.getExpression(), mi, preAuthorizeAttribute.getPostProcessor()));
// @formatter:on
}

Expand Down
Loading

0 comments on commit 9458473

Please sign in to comment.