From 50b85aea0d1d6c4bc7259cddf4d578a5266257bb Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 4 Apr 2024 10:20:41 -0600 Subject: [PATCH] Handle SpEL AuthorizationDeniedExceptions Closes gh-14600 --- ...rizationManagerAfterMethodInterceptor.java | 16 +++++- ...ManagerAfterReactiveMethodInterceptor.java | 28 ++++++++++- ...izationManagerBeforeMethodInterceptor.java | 19 ++++++- ...anagerBeforeReactiveMethodInterceptor.java | 47 ++++++++++++++--- .../MethodAuthorizationDeniedHandler.java | 15 ++++++ ...ethodAuthorizationDeniedPostProcessor.java | 18 +++++++ ...owingMethodAuthorizationDeniedHandler.java | 5 ++ ...ethodAuthorizationDeniedPostProcessor.java | 6 +++ ...ionManagerAfterMethodInterceptorTests.java | 23 +++++++++ ...erAfterReactiveMethodInterceptorTests.java | 50 +++++++++++++++---- ...onManagerBeforeMethodInterceptorTests.java | 21 ++++++++ ...rBeforeReactiveMethodInterceptorTests.java | 30 +++++++++-- .../authorization/method-security.adoc | 10 ++++ 13 files changed, 262 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java index d5ca8b5f16..0eaf7c51e9 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java @@ -30,6 +30,7 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; @@ -172,7 +173,13 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori private Object attemptAuthorization(MethodInvocation mi, Object result) { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); MethodInvocationResult object = new MethodInvocationResult(mi, result); - AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, object); + AuthorizationDecision decision; + try { + decision = this.authorizationManager.check(this::getAuthentication, object); + } + catch (AuthorizationDeniedException denied) { + return postProcess(object, denied); + } this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, object, decision); if (decision != null && !decision.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " @@ -183,6 +190,13 @@ public final class AuthorizationManagerAfterMethodInterceptor implements Authori return result; } + private Object postProcess(MethodInvocationResult mi, AuthorizationDeniedException denied) { + if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) { + return postProcessableDecision.postProcessResult(mi, denied); + } + return this.defaultPostProcessor.postProcessResult(mi, denied); + } + private Object postProcess(MethodInvocationResult mi, AuthorizationDecision decision) { if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) { return postProcessableDecision.postProcessResult(mi, decision); diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java index c1e6a297b1..2e56a3e865 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java @@ -34,6 +34,7 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -151,7 +152,32 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result); return this.authorizationManager.check(authentication, invocationResult) .switchIfEmpty(Mono.just(new AuthorizationDecision(false))) - .flatMap((decision) -> postProcess(decision, invocationResult)); + .materialize() + .flatMap((signal) -> { + if (!signal.hasError()) { + AuthorizationDecision decision = signal.get(); + return postProcess(decision, invocationResult); + } + if (signal.getThrowable() instanceof AuthorizationDeniedException denied) { + return postProcess(denied, invocationResult); + } + return Mono.error(signal.getThrowable()); + }); + } + + private Mono postProcess(AuthorizationDeniedException denied, + MethodInvocationResult methodInvocationResult) { + return Mono.fromSupplier(() -> { + if (this.authorizationManager instanceof MethodAuthorizationDeniedPostProcessor postProcessableDecision) { + return postProcessableDecision.postProcessResult(methodInvocationResult, denied); + } + return this.defaultPostProcessor.postProcessResult(methodInvocationResult, denied); + }).flatMap((processedResult) -> { + if (Mono.class.isAssignableFrom(processedResult.getClass())) { + return (Mono) processedResult; + } + return Mono.justOrEmpty(processedResult); + }); } private Mono postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) { 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 371c8a2c1d..4266efc68c 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 @@ -34,8 +34,10 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -245,7 +247,13 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author private Object attemptAuthorization(MethodInvocation mi) throws Throwable { this.logger.debug(LogMessage.of(() -> "Authorizing method invocation " + mi)); - AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, mi); + AuthorizationDecision decision; + try { + decision = this.authorizationManager.check(this::getAuthentication, mi); + } + catch (AuthorizationDeniedException denied) { + return handle(mi, denied); + } this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, mi, decision); if (decision != null && !decision.isGranted()) { this.logger.debug(LogMessage.of(() -> "Failed to authorize " + mi + " with authorization manager " @@ -256,7 +264,14 @@ public final class AuthorizationManagerBeforeMethodInterceptor implements Author return mi.proceed(); } - private Object handle(MethodInvocation mi, AuthorizationDecision decision) { + private Object handle(MethodInvocation mi, AuthorizationDeniedException denied) { + if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) { + return handler.handle(mi, denied); + } + return this.defaultHandler.handle(mi, denied); + } + + private Object handle(MethodInvocation mi, AuthorizationResult decision) { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) { return handler.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 6b1bab5f9d..e8d8179930 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 @@ -33,6 +33,7 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -140,11 +141,19 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); return this.authorizationManager.check(authentication, mi) .switchIfEmpty(Mono.just(new AuthorizationDecision(false))) - .flatMapMany((decision) -> { - if (decision.isGranted()) { - return mapping; + .materialize() + .flatMapMany((signal) -> { + if (!signal.hasError()) { + AuthorizationDecision decision = signal.get(); + if (decision.isGranted()) { + return mapping; + } + return postProcess(decision, mi); } - return postProcess(decision, mi); + if (signal.getThrowable() instanceof AuthorizationDeniedException denied) { + return postProcess(denied, mi); + } + return Mono.error(signal.getThrowable()); }); } @@ -152,14 +161,36 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); return this.authorizationManager.check(authentication, mi) .switchIfEmpty(Mono.just(new AuthorizationDecision(false))) - .flatMap((decision) -> { - if (decision.isGranted()) { - return mapping; + .materialize() + .flatMap((signal) -> { + if (!signal.hasError()) { + AuthorizationDecision decision = signal.get(); + if (decision.isGranted()) { + return mapping; + } + return postProcess(decision, mi); } - return postProcess(decision, mi); + if (signal.getThrowable() instanceof AuthorizationDeniedException denied) { + return postProcess(denied, mi); + } + return Mono.error(signal.getThrowable()); }); } + private Mono postProcess(AuthorizationDeniedException denied, MethodInvocation mi) { + return Mono.fromSupplier(() -> { + if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) { + return handler.handle(mi, denied); + } + return this.defaultHandler.handle(mi, denied); + }).flatMap((processedResult) -> { + if (Mono.class.isAssignableFrom(processedResult.getClass())) { + return (Mono) processedResult; + } + return Mono.justOrEmpty(processedResult); + }); + } + private Mono postProcess(AuthorizationDecision decision, MethodInvocation mi) { return Mono.fromSupplier(() -> { if (this.authorizationManager instanceof MethodAuthorizationDeniedHandler handler) { diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java index 8ccc797d98..051134f046 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedHandler.java @@ -19,6 +19,7 @@ package org.springframework.security.authorization.method; import org.aopalliance.intercept.MethodInvocation; import org.springframework.lang.Nullable; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationResult; /** @@ -43,4 +44,18 @@ public interface MethodAuthorizationDeniedHandler { @Nullable Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult); + /** + * Handle denied method invocations, implementations might either throw an + * {@link org.springframework.security.access.AccessDeniedException} or a replacement + * result instead of invoking the method, e.g. a masked value. + * @param methodInvocation the {@link MethodInvocation} related to the authorization + * denied + * @param authorizationDenied the authorization denied exception + * @return a replacement result for the denied method invocation, or null, or a + * {@link reactor.core.publisher.Mono} for reactive applications + */ + default Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) { + return handle(methodInvocation, authorizationDenied.getAuthorizationResult()); + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedPostProcessor.java b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedPostProcessor.java index a72b2b09e3..2d4c201309 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedPostProcessor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/MethodAuthorizationDeniedPostProcessor.java @@ -17,6 +17,7 @@ package org.springframework.security.authorization.method; import org.springframework.lang.Nullable; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationResult; /** @@ -43,4 +44,21 @@ public interface MethodAuthorizationDeniedPostProcessor { @Nullable Object postProcessResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult); + /** + * Post-process the denied result produced by a method invocation, implementations + * might either throw an + * {@link org.springframework.security.access.AccessDeniedException} or return a + * replacement result instead of the denied result, e.g. a masked value. + * @param methodInvocationResult the object containing the method invocation and the + * result produced + * @param authorizationDenied the {@link AuthorizationDeniedException} containing the + * authorization denied details + * @return a replacement result for the denied result, or null, or a + * {@link reactor.core.publisher.Mono} for reactive applications + */ + default Object postProcessResult(MethodInvocationResult methodInvocationResult, + AuthorizationDeniedException authorizationDenied) { + return postProcessResult(methodInvocationResult, authorizationDenied.getAuthorizationResult()); + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedHandler.java b/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedHandler.java index 374df81517..763cac16a2 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedHandler.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedHandler.java @@ -35,4 +35,9 @@ public final class ThrowingMethodAuthorizationDeniedHandler implements MethodAut throw new AuthorizationDeniedException("Access Denied", result); } + @Override + public Object handle(MethodInvocation methodInvocation, AuthorizationDeniedException authorizationDenied) { + throw authorizationDenied; + } + } diff --git a/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedPostProcessor.java b/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedPostProcessor.java index ab871576da..bb468a6e20 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedPostProcessor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/ThrowingMethodAuthorizationDeniedPostProcessor.java @@ -33,4 +33,10 @@ public final class ThrowingMethodAuthorizationDeniedPostProcessor implements Met throw new AuthorizationDeniedException("Access Denied", result); } + @Override + public Object postProcessResult(MethodInvocationResult methodInvocationResult, + AuthorizationDeniedException authorizationDenied) { + throw authorizationDenied; + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java index b82785f101..e34b45a33c 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptorTests.java @@ -26,8 +26,10 @@ import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; @@ -36,6 +38,7 @@ import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextImpl; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; @@ -139,4 +142,24 @@ public class AuthorizationManagerAfterMethodInterceptorTests { any(AuthorizationDecision.class)); } + @Test + public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable { + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.proceed()).willReturn("ok"); + AuthorizationManager manager = mock(AuthorizationManager.class); + given(manager.check(any(), any())) + .willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false))); + AuthorizationManagerAfterMethodInterceptor advice = new AuthorizationManagerAfterMethodInterceptor( + Pointcut.TRUE, manager); + assertThatExceptionOfType(MyAuthzDeniedException.class).isThrownBy(() -> advice.invoke(mi)); + } + + static class MyAuthzDeniedException extends AuthorizationDeniedException { + + MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) { + super(msg, authorizationResult); + } + + } + } 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 cef3ce38b7..9073d3eee5 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 @@ -28,6 +28,7 @@ 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; @@ -126,7 +127,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests { given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking); + given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class))) + .willAnswer(this::masking); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty()); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); @@ -145,13 +147,14 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests { given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); 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(argument.getResult()); - }); + given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class))) + .willAnswer((invocation) -> { + MethodInvocationResult argument = invocation.getArgument(0); + if (!"john".equals(argument.getResult())) { + return monoMasking(invocation); + } + return Mono.just(argument.getResult()); + }); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty()); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); @@ -170,7 +173,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests { given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::masking); + given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class))) + .willAnswer(this::masking); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty()); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); @@ -188,7 +192,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests { given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willAnswer(this::monoMasking); + given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class))) + .willAnswer(this::monoMasking); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty()); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); @@ -206,7 +211,8 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests { given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.postProcessResult(any(), any())).willReturn(null); + given(mockReactiveAuthorizationManager.postProcessResult(any(), any(AuthorizationResult.class))) + .willReturn(null); given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty()); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); @@ -235,6 +241,20 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests { verify(mockReactiveAuthorizationManager).check(any(), any()); } + @Test + public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("ok")); + ReactiveAuthorizationManager manager = mock(ReactiveAuthorizationManager.class); + given(manager.check(any(), any())) + .willReturn(Mono.error(new MyAuthzDeniedException("denied", new AuthorizationDecision(false)))); + AuthorizationManagerAfterReactiveMethodInterceptor advice = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, manager); + assertThatExceptionOfType(MyAuthzDeniedException.class) + .isThrownBy(() -> ((Mono) advice.invoke(mockMethodInvocation)).block()); + } + private Object masking(InvocationOnMock invocation) { MethodInvocationResult result = invocation.getArgument(0); return result.getResult() + "-masked"; @@ -262,4 +282,12 @@ public class AuthorizationManagerAfterReactiveMethodInterceptorTests { } + static class MyAuthzDeniedException extends AuthorizationDeniedException { + + MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) { + super(msg, authorizationResult); + } + + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java index 210a70e0b4..8022609ac7 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptorTests.java @@ -25,8 +25,10 @@ import org.springframework.aop.Pointcut; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; @@ -34,6 +36,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextImpl; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -133,4 +136,22 @@ public class AuthorizationManagerBeforeMethodInterceptorTests { any(AuthorizationDecision.class)); } + @Test + public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() { + AuthorizationManager manager = mock(AuthorizationManager.class); + given(manager.check(any(), any())) + .willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false))); + AuthorizationManagerBeforeMethodInterceptor advice = new AuthorizationManagerBeforeMethodInterceptor( + Pointcut.TRUE, manager); + assertThatExceptionOfType(MyAuthzDeniedException.class).isThrownBy(() -> advice.invoke(null)); + } + + static class MyAuthzDeniedException extends AuthorizationDeniedException { + + MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) { + super(msg, authorizationResult); + } + + } + } 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 54dfcd6ed8..0d5a71d000 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 @@ -27,6 +27,7 @@ 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; @@ -126,7 +127,7 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests { HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager.class); given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); - given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn("***"); + given(mockReactiveAuthorizationManager.handle(any(), any(AuthorizationResult.class))).willReturn("***"); AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); @@ -144,7 +145,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests { HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager.class); given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); - given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***")); + given(mockReactiveAuthorizationManager.handle(any(), any(AuthorizationResult.class))) + .willReturn(Mono.just("***")); AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); @@ -162,7 +164,8 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests { HandlingReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( HandlingReactiveAuthorizationManager.class); given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); - given(mockReactiveAuthorizationManager.handle(any(), any())).willReturn(Mono.just("***")); + given(mockReactiveAuthorizationManager.handle(any(), any(AuthorizationResult.class))) + .willReturn(Mono.just("***")); AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); @@ -209,6 +212,19 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests { verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); } + @Test + public void invokeWhenCustomAuthorizationDeniedExceptionThenThrows() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); + ReactiveAuthorizationManager manager = mock(ReactiveAuthorizationManager.class); + given(manager.check(any(), any())) + .willThrow(new MyAuthzDeniedException("denied", new AuthorizationDecision(false))); + AuthorizationManagerBeforeReactiveMethodInterceptor advice = new AuthorizationManagerBeforeReactiveMethodInterceptor( + Pointcut.TRUE, manager); + assertThatExceptionOfType(MyAuthzDeniedException.class) + .isThrownBy(() -> ((Mono) advice.invoke(mockMethodInvocation)).block()); + } + interface HandlingReactiveAuthorizationManager extends ReactiveAuthorizationManager, MethodAuthorizationDeniedHandler { @@ -226,4 +242,12 @@ public class AuthorizationManagerBeforeReactiveMethodInterceptorTests { } + static class MyAuthzDeniedException extends AuthorizationDeniedException { + + MyAuthzDeniedException(String msg, AuthorizationResult authorizationResult) { + super(msg, authorizationResult); + } + + } + } diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 0dbc4153de..ccd35bdb33 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -1249,6 +1249,9 @@ open class AuthorizationLogic { ---- ====== +Or throw a custom `AuthorizationDeniedException` instance. +Note, though, that returning an object is preferred as this doesn't incur the expense of generating a stacktrace. + Then, you can access the custom details when you <>. [[custom-authorization-managers]] @@ -1654,6 +1657,13 @@ Xml:: <4> This method may only be invoked by ``Princpal``s with an `aud` claim equal to "my-audience" <5> This method may only be invoked if the bean ``authz``'s `check` method returns `true` +[NOTE] +==== +You can use a bean like `authz` above to add programmatic authorization. +It can return a `boolean`, and `AuthorizationResult`, or throw an `AuthorizationDeniedException`. +For exceptions, you can <>. +==== + [[using_method_parameters]] === Using Method Parameters