From 77f2977c55842a717f8cb5c0344a7dd14b39c794 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 3 Apr 2024 13:20:26 -0600 Subject: [PATCH] Support SpEL Returning AuthorizationDecision Closes gh-14599 --- ...erringObservationAuthorizationManager.java | 31 ++++++- ...servationReactiveAuthorizationManager.java | 31 ++++++- .../method/configuration/Authz.java | 18 ++++ .../configuration/MethodSecurityService.java | 5 ++ .../MethodSecurityServiceConfig.java | 10 +++ .../MethodSecurityServiceImpl.java | 5 ++ ...ePostMethodSecurityConfigurationTests.java | 40 +++++++++ ...ctiveMethodSecurityConfigurationTests.java | 46 +++++++++- .../ReactiveMethodSecurityService.java | 5 ++ .../ReactiveMethodSecurityServiceImpl.java | 5 ++ .../access/expression/ExpressionUtils.java | 33 ++++++- .../ObservationAuthorizationManager.java | 30 ++++++- ...servationReactiveAuthorizationManager.java | 30 ++++++- ...rizationManagerAfterMethodInterceptor.java | 2 +- ...ManagerAfterReactiveMethodInterceptor.java | 2 +- ...izationManagerBeforeMethodInterceptor.java | 2 +- ...anagerBeforeReactiveMethodInterceptor.java | 2 +- .../PostAuthorizeAuthorizationDecision.java | 41 --------- .../PostAuthorizeAuthorizationManager.java | 17 ++-- ...AuthorizeReactiveAuthorizationManager.java | 17 +++- .../PreAuthorizeAuthorizationDecision.java | 43 ---------- .../PreAuthorizeAuthorizationManager.java | 16 ++-- ...AuthorizeReactiveAuthorizationManager.java | 16 +++- .../method/ReactiveExpressionUtils.java | 35 +++++++- .../expression/ExpressionUtilsTests.java | 70 +++++++++++++++ ...erAfterReactiveMethodInterceptorTests.java | 85 +++++++------------ ...rBeforeReactiveMethodInterceptorTests.java | 46 +++------- .../authorization/method-security.adoc | 36 ++++++++ 28 files changed, 520 insertions(+), 199 deletions(-) delete mode 100644 core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationDecision.java delete mode 100644 core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationDecision.java create mode 100644 core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationAuthorizationManager.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationAuthorizationManager.java index 4d534e5cfb4..05068d98a9a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationAuthorizationManager.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationAuthorizationManager.java @@ -19,18 +19,30 @@ 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 implements AuthorizationManager { +final class DeferringObservationAuthorizationManager + implements AuthorizationManager, MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor { private final Supplier> delegate; + private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler(); + + private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor(); + DeferringObservationAuthorizationManager(ObjectProvider provider, AuthorizationManager delegate) { this.delegate = SingletonSupplier.of(() -> { @@ -40,6 +52,12 @@ final class DeferringObservationAuthorizationManager 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 @@ public AuthorizationDecision check(Supplier authentication, T ob 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); + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationReactiveAuthorizationManager.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationReactiveAuthorizationManager.java index 9061cb64bb7..7600925bf25 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationReactiveAuthorizationManager.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/DeferringObservationReactiveAuthorizationManager.java @@ -19,19 +19,31 @@ 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 implements ReactiveAuthorizationManager { +final class DeferringObservationReactiveAuthorizationManager implements ReactiveAuthorizationManager, + MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor { private final Supplier> delegate; + private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler(); + + private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor(); + DeferringObservationReactiveAuthorizationManager(ObjectProvider provider, ReactiveAuthorizationManager delegate) { this.delegate = SingletonSupplier.of(() -> { @@ -41,6 +53,12 @@ final class DeferringObservationReactiveAuthorizationManager 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 @@ public Mono check(Mono authentication, T 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); + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java index 9a9ff57da49..145f344d126 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/Authz.java @@ -18,6 +18,8 @@ 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 boolean check(Authentication authentication, String message) { return message != null && message.contains(authentication.getName()); } + public AuthorizationResult checkResult(boolean result) { + return new AuthzResult(result); + } + + public Mono checkReactiveResult(boolean result) { + return Mono.just(checkResult(result)); + } + + public static class AuthzResult extends AuthorizationDecision { + + public AuthzResult(boolean granted) { + super(granted); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java index c3162debb3c..f8e1889c38d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityService.java @@ -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 diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java index ee664f5a45e..a5c78f6d962 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceConfig.java @@ -28,4 +28,14 @@ MethodSecurityService service() { return new MethodSecurityServiceImpl(); } + @Bean + ReactiveMethodSecurityService reactiveService() { + return new ReactiveMethodSecurityServiceImpl(); + } + + @Bean + Authz authz() { + return new Authz(); + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java index e44e9e048c6..4fd5b7fe1ad 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityServiceImpl.java @@ -197,4 +197,9 @@ public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() { return new UserRecordWithEmailProtected("username", "useremail@example.com"); } + @Override + public String checkCustomResult(boolean result) { + return "ok"; + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 72981bbd83c..f9b16350b60 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -66,6 +66,8 @@ 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.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 @@ void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() { 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 disallowBeanOverriding() { return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false); } @@ -1449,4 +1470,23 @@ public String getName() { } + @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; + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java index 60c54195bf8..1a2fc446a7c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfigurationTests.java @@ -47,6 +47,8 @@ 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.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 void findAllWhenNestedPreAuthorizeThenAuthorizes() { .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 authorities(String... authorities) { return (builder) -> builder.authorities(authorities); } @@ -353,4 +378,23 @@ public Mono getName() { } + @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; + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java index 836340a7eec..66c83348cb2 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityService.java @@ -85,6 +85,11 @@ public interface ReactiveMethodSecurityService { @Mask(expression = "@myMasker.getMask(returnObject)") Mono postAuthorizeWithMaskAnnotationUsingBean(); + @PreAuthorize(value = "@authz.checkReactiveResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class) + @PostAuthorize(value = "@authz.checkReactiveResult(!#result)", + postProcessorClass = MethodAuthorizationDeniedPostProcessor.class) + Mono checkCustomResult(boolean result); + class StarMaskingHandler implements MethodAuthorizationDeniedHandler { @Override diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java index 7fb421585aa..ce6a0204b64 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityServiceImpl.java @@ -82,4 +82,9 @@ public Mono postAuthorizeWithMaskAnnotationUsingBean() { return Mono.just("ok"); } + @Override + public Mono checkCustomResult(boolean result) { + return Mono.just("ok"); + } + } diff --git a/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java b/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java index 5296a3eacb9..1dff67ec985 100644 --- a/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/access/expression/ExpressionUtils.java @@ -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 @@ 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); diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java index 943e46dce3b..3f0050b0c49 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationAuthorizationManager.java @@ -21,11 +21,17 @@ 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 @@ * @author Josh Cummings * @since 6.0 */ -public final class ObservationAuthorizationManager implements AuthorizationManager, MessageSourceAware { +public final class ObservationAuthorizationManager implements AuthorizationManager, MessageSourceAware, + MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor { private final ObservationRegistry registry; @@ -46,9 +53,19 @@ public final class ObservationAuthorizationManager implements AuthorizationMa private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler(); + + private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor(); + public ObservationAuthorizationManager(ObservationRegistry registry, AuthorizationManager 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 void setMessageSource(final MessageSource 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); + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java index 83f4cb0609f..d84aa7e9969 100644 --- a/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/ObservationReactiveAuthorizationManager.java @@ -20,9 +20,15 @@ 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 @@ * @author Josh Cummings * @since 6.0 */ -public final class ObservationReactiveAuthorizationManager implements ReactiveAuthorizationManager { +public final class ObservationReactiveAuthorizationManager implements ReactiveAuthorizationManager, + MethodAuthorizationDeniedHandler, MethodAuthorizationDeniedPostProcessor { private final ObservationRegistry registry; @@ -40,10 +47,20 @@ public final class ObservationReactiveAuthorizationManager implements Reactiv private ObservationConvention> convention = new AuthorizationObservationConvention(); + private MethodAuthorizationDeniedHandler handler = new ThrowingMethodAuthorizationDeniedHandler(); + + private MethodAuthorizationDeniedPostProcessor postProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor(); + public ObservationReactiveAuthorizationManager(ObservationRegistry registry, ReactiveAuthorizationManager 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 void setObservationConvention(ObservationConvention postProcess(AuthorizationDecision decision, MethodInvocatio 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); diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java index aa96c2fb347..371c8a2c1d1 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java @@ -257,7 +257,7 @@ private Object attemptAuthorization(MethodInvocation mi) throws Throwable { } 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); diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java index 93bfb0d4a04..6b1bab5f9d1 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java @@ -162,7 +162,7 @@ private Mono preAuthorized(MethodInvocation mi, Mono mapping) { private Mono 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); diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationDecision.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationDecision.java deleted file mode 100644 index 136542cebe0..00000000000 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationDecision.java +++ /dev/null @@ -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); - } - -} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java index cd4f4065445..02022d2ccc3 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java @@ -27,6 +27,7 @@ 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 @@ * @author Evgeniy Cheban * @since 5.6 */ -public final class PostAuthorizeAuthorizationManager implements AuthorizationManager { +public final class PostAuthorizeAuthorizationManager + implements AuthorizationManager, MethodAuthorizationDeniedPostProcessor { private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry(); @@ -88,13 +90,18 @@ public AuthorizationDecision check(Supplier authentication, Meth 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); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java index 65b91ec81c4..4c988c05933 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java @@ -24,6 +24,7 @@ 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 @@ * @since 5.8 */ public final class PostAuthorizeReactiveAuthorizationManager - implements ReactiveAuthorizationManager { + implements ReactiveAuthorizationManager, MethodAuthorizationDeniedPostProcessor { private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry(); @@ -82,15 +83,23 @@ public Mono check(Mono authentication, Me if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return Mono.empty(); } - PostAuthorizeExpressionAttribute postAuthorizeAttribute = (PostAuthorizeExpressionAttribute) attribute; + MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); // @formatter:off return authentication .map((auth) -> expressionHandler.createEvaluationContext(auth, mi)) .doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx)) - .flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx)) - .map((granted) -> new 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); + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationDecision.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationDecision.java deleted file mode 100644 index f41e70b05ff..00000000000 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationDecision.java +++ /dev/null @@ -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); - } - -} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java index fc0a00857b3..fdab49b3402 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java @@ -27,6 +27,7 @@ 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 @@ * @author Evgeniy Cheban * @since 5.6 */ -public final class PreAuthorizeAuthorizationManager implements AuthorizationManager { +public final class PreAuthorizeAuthorizationManager + implements AuthorizationManager, MethodAuthorizationDeniedHandler { private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry(); @@ -80,11 +82,15 @@ public AuthorizationDecision check(Supplier authentication, Meth 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); } } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java index 9a640926d13..b9b9b2cc6e8 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java @@ -24,6 +24,7 @@ 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 @@ * @author Evgeniy Cheban * @since 5.8 */ -public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveAuthorizationManager { +public final class PreAuthorizeReactiveAuthorizationManager + implements ReactiveAuthorizationManager, MethodAuthorizationDeniedHandler { private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry(); @@ -79,13 +81,19 @@ public Mono check(Mono authentication, Me if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return Mono.empty(); } - PreAuthorizeExpressionAttribute preAuthorizeAttribute = (PreAuthorizeExpressionAttribute) attribute; // @formatter:off return authentication .map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi)) - .flatMap((ctx) -> ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx)) - .map((granted) -> new 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); + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java index 2675bb96dc7..6c80c0d364e 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java @@ -21,6 +21,8 @@ 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 @@ */ final class ReactiveExpressionUtils { + static Mono 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 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 evaluateAsBoolean(Expression expr, EvaluationContext ctx) { return Mono.defer(() -> { Object value; @@ -56,9 +85,9 @@ static Mono evaluateAsBoolean(Expression expr, EvaluationContext ctx) { }); } - private static Mono createInvalidReturnTypeMono(Expression expr) { - return Mono.error(() -> new IllegalStateException( - "Expression: '" + expr.getExpressionString() + "' must return boolean or Mono")); + private static Mono createInvalidReturnTypeMono(Expression expr) { + return Mono.error(() -> new IllegalStateException("Expression: '" + expr.getExpressionString() + + "' must return boolean, Mono, AuthorizationResult, or Mono")); } private ReactiveExpressionUtils() { diff --git a/core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java b/core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java new file mode 100644 index 00000000000..d54e1d32fba --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/expression/ExpressionUtilsTests.java @@ -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; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java index bf93e7351c8..cef3ce38b75 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java @@ -19,16 +19,15 @@ 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 void invokeFluxWhenAllValuesDeniedAndPostProcessorThenPostProcessorApplie MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); - ReactiveAuthorizationManager 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 void invokeFluxWhenOneValueDeniedAndPostProcessorThenPostProcessorApplied MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); - ReactiveAuthorizationManager 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 void invokeMonoWhenPostProcessableDecisionThenPostProcess() throws Throwa MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); - ReactiveAuthorizationManager 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 void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsMonoThenP MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); - ReactiveAuthorizationManager 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 void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsNullThenP MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); - ReactiveAuthorizationManager 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 void invokeMonoWhenEmptyDecisionThenUseDefaultPostProcessor() throws Thro 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, MethodAuthorizationDeniedPostProcessor { } diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java index d45bd42fa79..54dfcd6ed8f 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java @@ -23,12 +23,10 @@ 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 void invokeMonoWhenDeniedAndPostProcessorThenInvokePostProcessor() throws MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); - ReactiveAuthorizationManager 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 void invokeMonoWhenDeniedAndMonoPostProcessorThenInvokePostProcessor() th MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); - ReactiveAuthorizationManager 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 void invokeFluxWhenDeniedAndPostProcessorThenInvokePostProcessor() throws MethodInvocation mockMethodInvocation = spy( new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); - ReactiveAuthorizationManager 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 void invokeFluxWhenEmptyDecisionThenInvokeDefaultPostProcessor() throws T 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, MethodAuthorizationDeniedHandler { } diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 6bdd1ca4e90..2a6be2454df 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -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 <>. + [[custom-authorization-managers]] === Using a Custom Authorization Manager