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 java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationAuthorizationManager; 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.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier; 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 final Supplier<AuthorizationManager<T>> delegate;
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider, DeferringObservationAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
AuthorizationManager<T> delegate) { AuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> { this.delegate = SingletonSupplier.of(() -> {
@ -40,6 +52,12 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
} }
return new ObservationAuthorizationManager<>(registry, delegate); return new ObservationAuthorizationManager<>(registry, delegate);
}); });
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
} }
@Override @Override
@ -47,4 +65,15 @@ final class DeferringObservationAuthorizationManager<T> implements Authorization
return this.delegate.get().check(authentication, object); 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 java.util.function.Supplier;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ObservationReactiveAuthorizationManager; import org.springframework.security.authorization.ObservationReactiveAuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager; 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.security.core.Authentication;
import org.springframework.util.function.SingletonSupplier; 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 final Supplier<ReactiveAuthorizationManager<T>> delegate;
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider, DeferringObservationReactiveAuthorizationManager(ObjectProvider<ObservationRegistry> provider,
ReactiveAuthorizationManager<T> delegate) { ReactiveAuthorizationManager<T> delegate) {
this.delegate = SingletonSupplier.of(() -> { this.delegate = SingletonSupplier.of(() -> {
@ -41,6 +53,12 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
} }
return new ObservationReactiveAuthorizationManager<>(registry, delegate); return new ObservationReactiveAuthorizationManager<>(registry, delegate);
}); });
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
} }
@Override @Override
@ -48,4 +66,15 @@ final class DeferringObservationReactiveAuthorizationManager<T> implements React
return this.delegate.get().check(authentication, object); 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 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.security.core.Authentication;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -45,4 +47,20 @@ public class Authz {
return message != null && message.contains(authentication.getName()); 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) @PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized(); 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 { class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override @Override

View File

@ -28,4 +28,14 @@ public class MethodSecurityServiceConfig {
return new MethodSecurityServiceImpl(); 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"); 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.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObject; 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.MethodInvocationResult;
import org.springframework.security.authorization.method.PrePostTemplateDefaults; import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.Customizer; 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.atLeastOnce;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/** /**
* Tests for {@link PrePostMethodSecurityConfiguration}. * Tests for {@link PrePostMethodSecurityConfiguration}.
@ -925,6 +929,23 @@ public class PrePostMethodSecurityConfigurationTests {
assertThat(user.name()).isEqualTo("Protected"); 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() { private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); 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;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor; import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizeReturnObject; 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.Customizer;
import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext; 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.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.userdetails.User; 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.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 * @author Tadaya Tsuyukubo
@ -65,7 +73,7 @@ public class ReactiveMethodSecurityConfigurationTests {
public final SpringTestContext spring = new SpringTestContext(this); public final SpringTestContext spring = new SpringTestContext(this);
@Autowired @Autowired(required = false)
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler; DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;
@Test @Test
@ -212,6 +220,23 @@ public class ReactiveMethodSecurityConfigurationTests {
.verifyError(AccessDeniedException.class); .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) { private static Consumer<User.UserBuilder> authorities(String... authorities) {
return (builder) -> builder.authorities(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)") @Mask(expression = "@myMasker.getMask(returnObject)")
Mono<String> postAuthorizeWithMaskAnnotationUsingBean(); 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 { class StarMaskingHandler implements MethodAuthorizationDeniedHandler {
@Override @Override

View File

@ -82,4 +82,9 @@ public class ReactiveMethodSecurityServiceImpl implements ReactiveMethodSecurity
return Mono.just("ok"); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.EvaluationContext;
import org.springframework.expression.EvaluationException; import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression; 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 { public final class ExpressionUtils {
private 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) { public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
try { try {
return expr.getValue(ctx, Boolean.class); 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.Observation;
import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.ObservationConvention;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware; import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.access.AccessDeniedException; 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.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -36,7 +42,8 @@ import org.springframework.util.Assert;
* @author Josh Cummings * @author Josh Cummings
* @since 6.0 * @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; private final ObservationRegistry registry;
@ -46,9 +53,19 @@ public final class ObservationAuthorizationManager<T> implements AuthorizationMa
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
public ObservationAuthorizationManager(ObservationRegistry registry, AuthorizationManager<T> delegate) { public ObservationAuthorizationManager(ObservationRegistry registry, AuthorizationManager<T> delegate) {
this.registry = registry; this.registry = registry;
this.delegate = delegate; this.delegate = delegate;
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
} }
@Override @Override
@ -98,4 +115,15 @@ public final class ObservationAuthorizationManager<T> implements AuthorizationMa
this.messages = new MessageSourceAccessor(messageSource); 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.ObservationConvention;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.aopalliance.intercept.MethodInvocation;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.security.access.AccessDeniedException; 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.Authentication;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -32,7 +38,8 @@ import org.springframework.util.Assert;
* @author Josh Cummings * @author Josh Cummings
* @since 6.0 * @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; private final ObservationRegistry registry;
@ -40,10 +47,20 @@ public final class ObservationReactiveAuthorizationManager<T> implements Reactiv
private ObservationConvention<AuthorizationObservationContext<?>> convention = new AuthorizationObservationConvention(); private ObservationConvention<AuthorizationObservationContext<?>> convention = new AuthorizationObservationConvention();
private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler();
private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();
public ObservationReactiveAuthorizationManager(ObservationRegistry registry, public ObservationReactiveAuthorizationManager(ObservationRegistry registry,
ReactiveAuthorizationManager<T> delegate) { ReactiveAuthorizationManager<T> delegate) {
this.registry = registry; this.registry = registry;
this.delegate = delegate; this.delegate = delegate;
if (delegate instanceof MethodAuthorizationDeniedHandler h) {
this.handler = h;
}
if (delegate instanceof MethodAuthorizationDeniedPostProcessor p) {
this.postProcessor = p;
}
} }
@Override @Override
@ -81,4 +98,15 @@ public final class ObservationReactiveAuthorizationManager<T> implements Reactiv
this.convention = convention; 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) { private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) {
if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) { if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(mi, decision); return postProcessableDecision.postProcessResult(mi, decision);
} }
return this.defaultPostProcessor.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.just(methodInvocationResult.getResult());
} }
return Mono.fromSupplier(() -> { return Mono.fromSupplier(() -> {
if (decision instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) { if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) {
return postProcessableDecision.postProcessResult(methodInvocationResult, decision); return postProcessableDecision.postProcessResult(methodInvocationResult, decision);
} }
return this.defaultPostProcessor.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) { private Object handle(MethodInvocation mi, AuthorizationDecision decision) {
if (decision instanceof MethodAuthorizationDeniedHandler handler) { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, decision); return handler.handle(mi, decision);
} }
return this.defaultHandler.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) { private Mono<Object> postProcess(AuthorizationDecision decision, MethodInvocation mi) {
return Mono.fromSupplier(() -> { return Mono.fromSupplier(() -> {
if (decision instanceof MethodAuthorizationDeniedHandler handler) { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) {
return handler.handle(mi, decision); return handler.handle(mi, decision);
} }
return this.defaultHandler.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.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
/** /**
@ -37,7 +38,8 @@ import org.springframework.security.core.Authentication;
* @author Evgeniy Cheban * @author Evgeniy Cheban
* @since 5.6 * @since 5.6
*/ */
public final class PostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> { public final class PostAuthorizeAuthorizationManager
implements AuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry(); private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
@ -88,13 +90,18 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null; return null;
} }
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation()); EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
expressionHandler.setReturnObject(mi.getResult(), ctx); expressionHandler.setReturnObject(mi.getResult(), ctx);
boolean granted = ExpressionUtils.evaluateAsBoolean(postAuthorizeAttribute.getExpression(), ctx); return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
return new PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(), }
postAuthorizeAttribute.getPostProcessor());
@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.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -37,7 +38,7 @@ import org.springframework.util.Assert;
* @since 5.8 * @since 5.8
*/ */
public final class PostAuthorizeReactiveAuthorizationManager public final class PostAuthorizeReactiveAuthorizationManager
implements ReactiveAuthorizationManager<MethodInvocationResult> { implements ReactiveAuthorizationManager<MethodInvocationResult>, MethodAuthorizationDeniedPostProcessor {
private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry(); private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
@ -82,15 +83,23 @@ public final class PostAuthorizeReactiveAuthorizationManager
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return Mono.empty(); return Mono.empty();
} }
PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute;
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
// @formatter:off // @formatter:off
return authentication return authentication
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi)) .map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx)) .doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx)) .flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
.map((granted) -> new PostAuthorizeAuthorizationDecision(granted, postAuthorizeAttribute.getExpression(), postAuthorizeAttribute.getPostProcessor())); .cast(AuthorizationDecision.class);
// @formatter:on // @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.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
/** /**
@ -37,7 +38,8 @@ import org.springframework.security.core.Authentication;
* @author Evgeniy Cheban * @author Evgeniy Cheban
* @since 5.6 * @since 5.6
*/ */
public final class PreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> { public final class PreAuthorizeAuthorizationManager
implements AuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry(); private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
@ -80,11 +82,15 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null; return null;
} }
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi); EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
boolean granted = ExpressionUtils.evaluateAsBoolean(preAuthorizeAttribute.getExpression(), ctx); return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
return new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(), }
preAuthorizeAttribute.getHandler());
@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.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -36,7 +37,8 @@ import org.springframework.util.Assert;
* @author Evgeniy Cheban * @author Evgeniy Cheban
* @since 5.8 * @since 5.8
*/ */
public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveAuthorizationManager<MethodInvocation> { public final class PreAuthorizeReactiveAuthorizationManager
implements ReactiveAuthorizationManager<MethodInvocation>, MethodAuthorizationDeniedHandler {
private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry(); private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
@ -79,13 +81,19 @@ public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveA
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return Mono.empty(); return Mono.empty();
} }
PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute;
// @formatter:off // @formatter:off
return authentication return authentication
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi)) .map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx)) .flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
.map((granted) -> new PreAuthorizeAuthorizationDecision(granted, preAuthorizeAttribute.getExpression(), preAuthorizeAttribute.getHandler())); .cast(AuthorizationDecision.class);
// @formatter:on // @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.EvaluationContext;
import org.springframework.expression.EvaluationException; import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression; 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. * For internal use only, as this contract is likely to change.
@ -30,6 +32,33 @@ import org.springframework.expression.Expression;
*/ */
final class ReactiveExpressionUtils { 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) { static Mono<Boolean> evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
return Mono.defer(() -> { return Mono.defer(() -> {
Object value; Object value;
@ -56,9 +85,9 @@ final class ReactiveExpressionUtils {
}); });
} }
private static Mono<Boolean> createInvalidReturnTypeMono(Expression expr) { private static <T> Mono<T> createInvalidReturnTypeMono(Expression expr) {
return Mono.error(() -> new IllegalStateException( return Mono.error(() -> new IllegalStateException("Expression: '" + expr.getExpressionString()
"Expression: '" + expr.getExpressionString() + "' must return boolean or Mono<Boolean>")); + "' must return boolean, Mono<Boolean>, AuthorizationResult, or Mono<AuthorizationResult>"));
} }
private ReactiveExpressionUtils() { 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.aopalliance.intercept.MethodInvocation;
import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.invocation.InvocationOnMock;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.aop.Pointcut; import org.springframework.aop.Pointcut;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.intercept.method.MockMethodInvocation; import org.springframework.security.access.intercept.method.MockMethodInvocation;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.authorization.ReactiveAuthorizationManager;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -125,10 +124,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy( MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class); HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), any())) given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
.will((invocation) -> Mono.just(createDecision(new MaskingPostProcessor()))); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager); Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation); Object result = interceptor.invoke(mockMethodInvocation);
@ -144,15 +143,16 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy( MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class); HandlingReactiveAuthorizationManager.class);
given(mockReactiveAuthorizationManager.check(any(), any())).willAnswer((invocation) -> { given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer((invocation) -> {
MethodInvocationResult argument = invocation.getArgument(1); MethodInvocationResult argument = invocation.getArgument(0);
if ("john".equals(argument.getResult())) { if (!"john".equals(argument.getResult())) {
return Mono.just(new AuthorizationDecision(true)); 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( AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager); Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation); Object result = interceptor.invoke(mockMethodInvocation);
@ -168,11 +168,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy( MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class); HandlingReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false, given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking);
new LiteralExpression("1234"), new MaskingPostProcessor()); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager); Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation); Object result = interceptor.invoke(mockMethodInvocation);
@ -187,11 +186,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy( MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class); HandlingReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false, given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::monoMasking);
new LiteralExpression("1234"), new MonoMaskingPostProcessor()); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager); Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation); Object result = interceptor.invoke(mockMethodInvocation);
@ -206,11 +204,10 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
MethodInvocation mockMethodInvocation = spy( MethodInvocation mockMethodInvocation = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock(
ReactiveAuthorizationManager.class); HandlingReactiveAuthorizationManager.class);
PostAuthorizeAuthorizationDecision decision = new PostAuthorizeAuthorizationDecision(false, given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willReturn(null);
new LiteralExpression("1234"), new NullPostProcessor()); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty());
given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision));
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
Pointcut.TRUE, mockReactiveAuthorizationManager); Pointcut.TRUE, mockReactiveAuthorizationManager);
Object result = interceptor.invoke(mockMethodInvocation); Object result = interceptor.invoke(mockMethodInvocation);
@ -238,34 +235,18 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests {
verify(mockReactiveAuthorizationManager).check(any(), any()); verify(mockReactiveAuthorizationManager).check(any(), any());
} }
private PostAuthorizeAuthorizationDecision createDecision(MethodAuthorizationDeniedPostProcessor postProcessor) { private Object masking(InvocationOnMock invocation) {
return new PostAuthorizeAuthorizationDecision(false, new LiteralExpression("1234"), postProcessor); MethodInvocationResult result = invocation.getArgument(0);
return result.getResult() + "-masked";
} }
static class MaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor { private Object monoMasking(InvocationOnMock invocation) {
MethodInvocationResult result = invocation.getArgument(0);
@Override return Mono.just(result.getResult() + "-masked");
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return contextObject.getResult() + "-masked";
}
} }
static class MonoMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor { interface HandlingReactiveAuthorizationManager
extends ReactiveAuthorizationManager<MethodInvocationResult>, 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;
}
} }

View File

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

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. 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. 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]] [[custom-authorization-managers]]
=== Using a Custom Authorization Manager === Using a Custom Authorization Manager