Support SpEL Returning AuthorizationDecision

Closes gh-14598
This commit is contained in:
Josh Cummings 2024-04-03 13:20:26 -06:00
parent 0a9c482f62
commit 6f07d63938
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
28 changed files with 520 additions and 199 deletions

View File

@ -19,18 +19,30 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationAuthorizationManager;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier;
final class DeferringObservationAuthorizationManager<T> implements AuthorizationManager<T> {
final class DeferringObservationAuthorizationManager<T>
implements AuthorizationManager<T>, MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
private final Supplier<AuthorizationManager<T>> delegate;
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
AuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> {
@ -40,6 +52,12 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
}
return new ObservationAuthorizationManager<>(registry, delegate);
});
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
}
@Override
@ -47,4 +65,15 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
return this.delegate.get().check(authentication, object);
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handle(methodInvocation, authorizationResult);
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
}
}

View File

@ -19,19 +19,31 @@ package org.springframework.security.config.annotation.method.configuration;
import java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier;
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
final class DeferringObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T>,
MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
private final Supplier<ReactiveAuthorizationManager<T>> delegate;
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
ReactiveAuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> {
@ -41,6 +53,12 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
}
return new ObservationReactiveAuthorizationManager<>(registry, delegate);
});
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
}
@Override
@ -48,4 +66,15 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
return this.delegate.get().check(authentication, object);
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handle(methodInvocation, authorizationResult);
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
}
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.method.configuration;
import reactor.core.publisher.Mono;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
@ -45,4 +47,20 @@ public class Authz {
return message != null && message.contains(authentication.getName());
}
public AuthorizationResult checkResult(boolean result) {
return new AuthzResult(result);
}
public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
return Mono.just(checkResult(result));
}
public static class AuthzResult extends AuthorizationDecision {
public AuthzResult(boolean granted) {
super(granted);
}
}
}

View File

@ -173,6 +173,11 @@ public interface MethodSecurityService {
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();
@PreAuthorize(value = "@authz.checkResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
@PostAuthorize(value = "@authz.checkResult(!#result)",
postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
String checkCustomResult(boolean result);
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override

View File

@ -28,4 +28,14 @@ public class MethodSecurityServiceConfig {
return new MethodSecurityServiceImpl();
}
@Bean
ReactiveMethodSecurityService reactiveService() {
return new ReactiveMethodSecurityServiceImpl();
}
@Bean
Authz authz() {
return new Authz();
}
}

View File

@ -197,4 +197,9 @@ public class MethodSecurityServiceImpl implements MethodSecurityService {
return new UserRecordWithEmailProtected("username", "useremail@example.com");
}
@Override
public String checkCustomResult(boolean result) {
return "ok";
}
}

View File

@ -66,6 +66,8 @@ import org.springframework.security.authorization.method.AuthorizationAdvisorPro
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.Customizer;
@ -92,6 +94,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link PrePostMethodSecurityConfiguration}.
@ -925,6 +929,23 @@ public class PrePostMethodSecurityConfigurationTests {
assertThat(user.name()).isEqualTo("Protected");
}
@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
.getBean(MethodAuthorizationDeniedPostProcessor.class);
assertThat(service.checkCustomResult(false)).isNull();
verify(handler).handle(any(), any(Authz.AuthzResult.class));
verifyNoInteractions(postProcessor);
assertThat(service.checkCustomResult(true)).isNull();
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
verifyNoMoreInteractions(handler);
}
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
@ -1449,4 +1470,23 @@ public class PrePostMethodSecurityConfigurationTests {
}
@EnableMethodSecurity
static class CustomResultConfig {
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);
@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}
@Bean
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
return this.postProcessor;
}
}
}

View File

@ -47,6 +47,8 @@ import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext;
@ -54,8 +56,14 @@ import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* @author Tadaya Tsuyukubo
@ -65,7 +73,7 @@ public class ReactiveMethodSecurityConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this);
@Autowired
@Autowired(required = false)
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;
@Test
@ -212,6 +220,23 @@ public class ReactiveMethodSecurityConfigurationTests {
.verifyError(AccessDeniedException.class);
}
@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
.getBean(MethodAuthorizationDeniedPostProcessor.class);
assertThat(service.checkCustomResult(false).block()).isNull();
verify(handler).handle(any(), any(Authz.AuthzResult.class));
verifyNoInteractions(postProcessor);
assertThat(service.checkCustomResult(true).block()).isNull();
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
verifyNoMoreInteractions(handler);
}
private static Consumer<User.UserBuilder> authorities(String... authorities) {
return (builder) -> builder.authorities(authorities);
}
@ -353,4 +378,23 @@ public class ReactiveMethodSecurityConfigurationTests {
}
@EnableReactiveMethodSecurity
static class CustomResultConfig {
MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);
@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}
@Bean
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
return this.postProcessor;
}
}
}

View File

@ -85,6 +85,11 @@ public interface ReactiveMethodSecurityService {
@Mask(expression = "@myMasker.getMask(returnObject)")
Mono<String> postAuthorizeWithMaskAnnotationUsingBean();
@PreAuthorize(value = "@authz.checkReactiveResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
@PostAuthorize(value = "@authz.checkReactiveResult(!#result)",
postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
Mono<String> checkCustomResult(boolean result);
class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override

View File

@ -82,4 +82,9 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurity
return Mono.just("ok");
}
@Override
public Mono<String> checkCustomResult(boolean result) {
return Mono.just("ok");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,12 +19,43 @@ package org.springframework.security.access.expression;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
public final class ExpressionUtils {
private ExpressionUtils() {
}
/**
* Evaluate a SpEL expression and coerce into an {@link AuthorizationDecision}
* @param expr a SpEL expression
* @param ctx an {@link EvaluationContext}
* @return the resulting {@link AuthorizationDecision}
* @since 6.3
*/
public static AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) {
try {
Object result = expr.getValue(ctx);
if (result instanceof AuthorizationResult decision) {
return decision;
}
if (result instanceof Boolean granted) {
return new ExpressionAuthorizationDecision(granted, expr);
}
if (result == null) {
return null;
}
throw new IllegalArgumentException(
"SpEL expression must return either a Boolean or an AuthorizationDecision");
}
catch (EvaluationException ex) {
throw new IllegalArgumentException("Failed to evaluate expression '" + expr.getExpressionString() + "'",
ex);
}
}
public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
try {
return expr.getValue(ctx, Boolean.class);

View File

@ -21,11 +21,17 @@ import java.util.function.Supplier;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
@ -36,7 +42,8 @@ import org.springframework.util.Assert;
* @author Josh Cummings
* @since 6.0
*/
public final class ObservationAuthorizationManager<T> implements AuthorizationManager<T>, MessageSourceAware {
public final class ObservationAuthorizationManager<T> implements AuthorizationManager<T>, MessageSourceAware,
MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
private final ObservationRegistry registry;
@ -46,9 +53,19 @@ public final class ObservationAuthorizationManager<T> implements AuthorizationMa
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
public ObservationAuthorizationManager(ObservationRegistry registry, AuthorizationManager<T> delegate) {
this.registry = registry;
this.delegate = delegate;
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
}
@Override
@ -98,4 +115,15 @@ public final class ObservationAuthorizationManager<T> implements AuthorizationMa
this.messages = new MessageSourceAccessor(messageSource);
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handle(methodInvocation, authorizationResult);
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
}
}

View File

@ -20,9 +20,15 @@ import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
@ -32,7 +38,8 @@ import org.springframework.util.Assert;
* @author Josh Cummings
* @since 6.0
*/
public final class ObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
public final class ObservationReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T>,
MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor {
private final ObservationRegistry registry;
@ -40,10 +47,20 @@ public final class ObservationReactiveAuthorizationManager<T> implements Reactiv
private ObservationConvention<AuthorizationObservationContext<?>> convention = new AuthorizationObservationConvention();
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
public ObservationReactiveAuthorizationManager(ObservationRegistry registry,
ReactiveAuthorizationManager<T> delegate) {
this.registry = registry;
this.delegate = delegate;
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
}
@Override
@ -81,4 +98,15 @@ public final class ObservationReactiveAuthorizationManager<T> implements Reactiv
this.convention = convention;
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return this.handler.handle(methodInvocation, authorizationResult);
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return this.postProcessor.postProcessResult(methodInvocationResult, authorizationResult);
}
}

View File

@ -184,7 +184,7 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori
}
private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) {
if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(mi, decision);
}
return this.defaultPostProcessor.postProcessResult(mi, decision);

View File

@ -159,7 +159,7 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements
return Mono.just(methodInvocationResult.getResult());
}
return Mono.fromSupplier(() -> {
if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(methodInvocationResult, decision);
}
return this.defaultPostProcessor.postProcessResult(methodInvocationResult, decision);

View File

@ -257,7 +257,7 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author
}
private Object handle(MethodInvocation mi, AuthorizationDecision decision) {
if (decision instanceof MethodAuthorizationDeniedHandler handler) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, decision);
}
return this.defaultHandler.handle(mi, decision);

View File

@ -162,7 +162,7 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement
private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
return Mono.fromSupplier(() -> {
if (decision instanceof MethodAuthorizationDeniedHandler handler) {
if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, decision);
}
return this.defaultHandler.handle(mi, decision);

View File

@ -1,41 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.util.Assert;
class PostAuthorizeAuthorizationDecision extends ExpressionAuthorizationDecision
implements MethodAuthorizationDeniedPostProcessor {
private final MethodAuthorizationDeniedPostProcessor postProcessor;
PostAuthorizeAuthorizationDecision(boolean granted, Expression expression,
MethodAuthorizationDeniedPostProcessor postProcessor) {
super(granted, expression);
Assert.notNull(postProcessor, "postProcessor cannot be null");
this.postProcessor = postProcessor;
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult result) {
return this.postProcessor.postProcessResult(methodInvocationResult, result);
}
}

View File

@ -27,6 +27,7 @@ import org.springframework.security.access.expression.method.MethodSecurityExpre
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
/**
@ -37,7 +38,8 @@ import org.springframework.security.core.Authentication;
* @author Evgeniy Cheban
* @since 5.6
*/
public final class PostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
public final class PostAuthorizeAuthorizationManager
implements AuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
@ -88,13 +90,18 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null;
}
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
expressionHandler.setReturnObject(mi.getResult(), ctx);
boolean granted = ExpressionUtils.evaluateAsBoolean(postAuthorizeAttribute.getExpression(), ctx);
return new PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(),
postAuthorizeAttribute.getPostProcessor());
return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
ExpressionAttribute attribute = this.registry.getAttribute(methodInvocationResult.getMethodInvocation());
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
return postAuthorizeAttribute.getPostProcessor().postProcessResult(methodInvocationResult, authorizationResult);
}
}

View File

@ -24,6 +24,7 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
@ -37,7 +38,7 @@ import org.springframework.util.Assert;
* @since 5.8
*/
public final class PostAuthorizeReactiveAuthorizationManager
implements ReactiveAuthorizationManager<MethodInvocationResult> {
implements ReactiveAuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
@ -82,15 +83,23 @@ public final class PostAuthorizeReactiveAuthorizationManager
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 PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(), postAuthorizeAttribute.getPostProcessor()));
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
.cast(AuthorizationDecision.class);
// @formatter:on
}
@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
ExpressionAttribute attribute = this.registry.getAttribute(methodInvocationResult.getMethodInvocation());
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
return postAuthorizeAttribute.getPostProcessor().postProcessResult(methodInvocationResult, authorizationResult);
}
}

View File

@ -1,43 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.util.Assert;
class PreAuthorizeAuthorizationDecision extends ExpressionAuthorizationDecision
implements MethodAuthorizationDeniedHandler {
private final MethodAuthorizationDeniedHandler handler;
PreAuthorizeAuthorizationDecision(boolean granted, Expression expression,
MethodAuthorizationDeniedHandler handler) {
super(granted, expression);
Assert.notNull(handler, "handler cannot be null");
this.handler = handler;
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return this.handler.handle(methodInvocation, result);
}
}

View File

@ -27,6 +27,7 @@ import org.springframework.security.access.expression.method.MethodSecurityExpre
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
/**
@ -37,7 +38,8 @@ import org.springframework.security.core.Authentication;
* @author Evgeniy Cheban
* @since 5.6
*/
public final class PreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
public final class PreAuthorizeAuthorizationManager
implements AuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
@ -80,11 +82,15 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null;
}
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
boolean granted = ExpressionUtils.evaluateAsBoolean(preAuthorizeAttribute.getExpression(), ctx);
return new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(),
preAuthorizeAttribute.getHandler());
return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation);
PreAuthorizeExpressionAttribute postAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
return postAuthorizeAttribute.getHandler().handle(methodInvocation, authorizationResult);
}
}

View File

@ -24,6 +24,7 @@ import org.springframework.security.access.expression.method.DefaultMethodSecuri
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
@ -36,7 +37,8 @@ import org.springframework.util.Assert;
* @author Evgeniy Cheban
* @since 5.8
*/
public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> {
public final class PreAuthorizeReactiveAuthorizationManager
implements ReactiveAuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
@ -79,13 +81,19 @@ public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveA
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 PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(), preAuthorizeAttribute.getHandler()));
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
.cast(AuthorizationDecision.class);
// @formatter:on
}
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
ExpressionAttribute attribute = this.registry.getAttribute(methodInvocation);
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
return preAuthorizeAttribute.getHandler().handle(methodInvocation, authorizationResult);
}
}

View File

@ -21,6 +21,8 @@ import reactor.core.publisher.Mono;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
/**
* For internal use only, as this contract is likely to change.
@ -30,6 +32,33 @@ import org.springframework.expression.Expression;
*/
final class ReactiveExpressionUtils {
static Mono<AuthorizationResult> evaluate(Expression expr, EvaluationContext ctx) {
return Mono.defer(() -> {
Object value;
try {
value = expr.getValue(ctx);
}
catch (EvaluationException ex) {
return Mono.error(() -> new IllegalArgumentException(
"Failed to evaluate expression '" + expr.getExpressionString() + "'", ex));
}
if (value instanceof Mono<?> mono) {
return mono.flatMap((data) -> adapt(expr, data));
}
return adapt(expr, value);
});
}
private static Mono<AuthorizationResult> adapt(Expression expr, Object value) {
if (value instanceof Boolean granted) {
return Mono.just(new ExpressionAuthorizationDecision(granted, expr));
}
if (value instanceof AuthorizationResult decision) {
return Mono.just(decision);
}
return createInvalidReturnTypeMono(expr);
}
static Mono<Boolean> evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
return Mono.defer(() -> {
Object value;
@ -56,9 +85,9 @@ final class ReactiveExpressionUtils {
});
}
private static Mono<Boolean> createInvalidReturnTypeMono(Expression expr) {
return Mono.error(() -> new IllegalStateException(
"Expression: '" + expr.getExpressionString() + "' must return boolean or Mono<Boolean>"));
private static <T> Mono<T> createInvalidReturnTypeMono(Expression expr) {
return Mono.error(() -> new IllegalStateException("Expression: '" + expr.getExpressionString()
+ "' must return boolean, Mono<Boolean>, AuthorizationResult, or Mono<AuthorizationResult>"));
}
private ReactiveExpressionUtils() {

View File

@ -0,0 +1,70 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.access.expression;
import org.junit.jupiter.api.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import static org.assertj.core.api.Assertions.assertThat;
public class ExpressionUtilsTests {
private final Object details = new Object();
@Test
public void evaluateWhenAuthorizationDecisionThenReturns() {
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("#root.returnDecision()");
StandardEvaluationContext context = new StandardEvaluationContext(this);
assertThat(ExpressionUtils.evaluate(expression, context)).isInstanceOf(AuthorizationDecisionDetails.class)
.extracting("details")
.isEqualTo(this.details);
}
@Test
public void evaluateWhenBooleanThenReturnsExpressionAuthorizationDecision() {
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("#root.returnResult()");
StandardEvaluationContext context = new StandardEvaluationContext(this);
assertThat(ExpressionUtils.evaluate(expression, context)).isInstanceOf(ExpressionAuthorizationDecision.class);
}
public AuthorizationDecision returnDecision() {
return new AuthorizationDecisionDetails(false, this.details);
}
public boolean returnResult() {
return false;
}
static final class AuthorizationDecisionDetails extends AuthorizationDecision {
final Object details;
AuthorizationDecisionDetails(boolean granted, Object details) {
super(granted);
this.details = details;
}
}
}

View File

@ -19,16 +19,15 @@ package org.springframework.security.authorization.method;
import org.aopalliance.intercept.MethodInvocation;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.mockito.invocation.InvocationOnMock;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.aop.Pointcut;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.intercept.method.MockMethodInvocation;
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 static org.assertj.core.api.Assertions.assertThat;
@ -125,10 +124,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), any()))
.will((invocation) -> Mono.just(createDecision(new MaskingPostProcessor())));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -144,15 +143,16 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), any())).willAnswer((invocation) -> {
MethodInvocationResult argument = invocation.getArgument(1);
if ("john".equals(argument.getResult())) {
return Mono.just(new AuthorizationDecision(true));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer((invocation) -> {
MethodInvocationResult argument = invocation.getArgument(0);
if (!"john".equals(argument.getResult())) {
return monoMasking(invocation);
}
return Mono.just(createDecision(new MaskingPostProcessor()));
return Mono.just(argument.getResult());
});
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -168,11 +168,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -187,11 +186,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MonoMaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::monoMasking);
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -206,11 +204,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new NullPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willReturn(null);
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -238,34 +235,18 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
verify(mockReactiveAuthorizationManager).check(any(), any());
}
private PostAuthorizeAuthorizationDecision createDecision(MethodAuthorizationDeniedPostProcessor postProcessor) {
return new PostAuthorizeAuthorizationDecision(false, new LiteralExpression("1234"), postProcessor);
private Object masking(InvocationOnMock invocation) {
MethodInvocationResult result = invocation.getArgument(0);
return result.getResult() + "-masked";
}
static class MaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return contextObject.getResult() + "-masked";
}
private Object monoMasking(InvocationOnMock invocation) {
MethodInvocationResult result = invocation.getArgument(0);
return Mono.just(result.getResult() + "-masked");
}
static class MonoMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return Mono.just(contextObject.getResult() + "-masked");
}
}
static class NullPostProcessor implements MethodAuthorizationDeniedPostProcessor {
@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return null;
}
interface HandlingReactiveAuthorizationManager
extends ReactiveAuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
}

View File

@ -23,12 +23,10 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.aop.Pointcut;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.intercept.method.MockMethodInvocation;
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 static org.assertj.core.api.Assertions.assertThat;
@ -125,11 +123,10 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn("***");
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -144,11 +141,10 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MonoMaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***"));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -163,11 +159,10 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocation> mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class);
PreAuthorizeAuthorizationDecision decision = new PreAuthorizeAuthorizationDecision(false,
new LiteralExpression("1234"), new MonoMaskingPostProcessor());
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision));
HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***"));
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation);
@ -214,21 +209,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests {
verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation));
}
static class MaskingPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return "***";
}
}
static class MonoMaskingPostProcessor implements MethodAuthorizationDeniedHandler {
@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return Mono.just("***");
}
interface HandlingReactiveAuthorizationManager
extends ReactiveAuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
}

View File

@ -1215,6 +1215,42 @@ Spring Security will invoke the given method on that bean for each method invoca
What's nice about this is all your authorization logic is in a separate class that can be independently unit tested and verified for correctness.
It also has access to the full Java language.
[TIP]
In addition to returning a `Boolean`, you can also return `null` to indicate that the code abstains from making a decision.
If you want to include more information about the nature of the decision, you can instead return a custom `AuthorizationDecision` like this:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Component("authz")
public class AuthorizationLogic {
public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
return new MyAuthorizationDecision(false, details);
}
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Component("authz")
open class AuthorizationLogic {
fun decide(val operations: MethodSecurityExpressionOperations): AuthorizationDecision {
// ... authorization logic
return MyAuthorizationDecision(false, details)
}
}
----
======
Then, you can access the custom details when you <<fallback-values-authorization-denied, customize how the authorization result is handled>>.
[[custom-authorization-managers]]
=== Using a Custom Authorization Manager