diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java index 2bfa745f676..8e777e8bc4c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; @@ -74,9 +75,10 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityE static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor( MethodSecurityExpressionHandler expressionHandler, ObjectProvider defaultsObjectProvider, - ObjectProvider registryProvider) { + ObjectProvider registryProvider, ApplicationContext context) { PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager( expressionHandler); + manager.setApplicationContext(context); ReactiveAuthorizationManager authorizationManager = manager(manager, registryProvider); AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor .preAuthorize(authorizationManager); @@ -99,9 +101,10 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurity static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( MethodSecurityExpressionHandler expressionHandler, ObjectProvider defaultsObjectProvider, - ObjectProvider registryProvider) { + ObjectProvider registryProvider, ApplicationContext context) { PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager( expressionHandler); + manager.setApplicationContext(context); ReactiveAuthorizationManager authorizationManager = manager(manager, registryProvider); AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor .postAuthorize(authorizationManager); diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java index fa1f17c011b..bb24d12588a 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationDeniedPostProcessor.java @@ -21,6 +21,13 @@ public interface AuthorizationDeniedPostProcessor { + /** + * @param contextObject the object containing context information for an authorization + * decision + * @param result the {@link AuthorizationResult} containing the authorization details + * @return the object to be returned to the component that is not authorized, can also + * be an instance of {@link reactor.core.publisher.Mono}. + */ @Nullable Object postProcessResult(T contextObject, AuthorizationResult result); 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 ea3d0236d8c..64554301386 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 @@ -33,6 +33,9 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -57,6 +60,8 @@ public final class AuthorizationManagerAfterReactiveMethodInterceptor implements private int order = AuthorizationInterceptorsOrder.LAST.getOrder(); + private final AuthorizationDeniedPostProcessor postProcessor = new ReactiveAuthorizationDeniedPostProcessorAdapter(); + /** * Creates an instance for the {@link PostAuthorize} annotation. * @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use @@ -144,9 +149,28 @@ private boolean isMultiValue(Class returnType, ReactiveAdapter adapter) { return adapter != null && adapter.isMultiValue(); } - private Mono postAuthorize(Mono authentication, MethodInvocation mi, Object result) { - return this.authorizationManager.verify(authentication, new MethodInvocationResult(mi, result)) - .thenReturn(result); + private Mono postAuthorize(Mono authentication, MethodInvocation mi, Object result) { + MethodInvocationResult invocationResult = new MethodInvocationResult(mi, result); + return this.authorizationManager.check(authentication, invocationResult) + .switchIfEmpty(Mono.just(new AuthorizationDecision(false))) + .flatMap((decision) -> postProcess(decision, invocationResult)); + } + + private Mono postProcess(AuthorizationDecision decision, MethodInvocationResult methodInvocationResult) { + if (decision.isGranted()) { + return Mono.just(methodInvocationResult.getResult()); + } + return Mono.fromSupplier(() -> { + if (decision instanceof PostProcessableAuthorizationDecision postProcessableDecision) { + return postProcessableDecision.postProcess(); + } + return this.postProcessor.postProcessResult(methodInvocationResult, decision); + }).flatMap((processedResult) -> { + if (Mono.class.isAssignableFrom(processedResult.getClass())) { + return (Mono) processedResult; + } + return Mono.justOrEmpty(processedResult); + }); } @Override @@ -184,4 +208,14 @@ private static Object asFlow(Publisher publisher) { } + private static class ReactiveAuthorizationDeniedPostProcessorAdapter + implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) { + return Mono.error(new AuthorizationDeniedException("Access Denied", result)); + } + + } + } 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 25e43e15883..dea96094c66 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 @@ -32,6 +32,9 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDeniedException; +import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.ReactiveAuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.util.Assert; @@ -57,6 +60,8 @@ public final class AuthorizationManagerBeforeReactiveMethodInterceptor implement private int order = AuthorizationInterceptorsOrder.FIRST.getOrder(); + private final AuthorizationDeniedPostProcessor postProcessor = new ReactiveAuthorizationDeniedPostProcessorAdapter(); + /** * Creates an instance for the {@link PreAuthorize} annotation. * @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use @@ -112,31 +117,65 @@ public Object invoke(MethodInvocation mi) throws Throwable { + " must return an instance of org.reactivestreams.Publisher " + "(for example, a Mono or Flux) or the function must be a Kotlin coroutine " + "in order to support Reactor Context"); - Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type); - Mono preAuthorize = this.authorizationManager.verify(authentication, mi); if (hasFlowReturnType) { if (isSuspendingFunction) { - return preAuthorize.thenMany(Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi))); + return preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi))); } else { Assert.state(adapter != null, () -> "The returnType " + type + " on " + method + " must have a org.springframework.core.ReactiveAdapter registered"); - Flux response = preAuthorize - .thenMany(Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi)))); + Flux response = preAuthorized(mi, + Flux.defer(() -> adapter.toPublisher(ReactiveMethodInvocationUtils.proceed(mi)))); return KotlinDelegate.asFlow(response); } } if (isMultiValue(type, adapter)) { - Publisher publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)); - Flux result = preAuthorize.thenMany(publisher); + Flux result = preAuthorized(mi, Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi))); return (adapter != null) ? adapter.fromPublisher(result) : result; } - Mono publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)); - Mono result = preAuthorize.then(publisher); + Mono result = preAuthorized(mi, Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi))); return (adapter != null) ? adapter.fromPublisher(result) : result; } + private Flux preAuthorized(MethodInvocation mi, Flux mapping) { + Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); + return this.authorizationManager.check(authentication, mi) + .switchIfEmpty(Mono.just(new AuthorizationDecision(false))) + .flatMapMany((decision) -> { + if (decision.isGranted()) { + return mapping; + } + return postProcess(decision, mi); + }); + } + + private Mono preAuthorized(MethodInvocation mi, Mono mapping) { + Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); + return this.authorizationManager.check(authentication, mi) + .switchIfEmpty(Mono.just(new AuthorizationDecision(false))) + .flatMap((decision) -> { + if (decision.isGranted()) { + return mapping; + } + return postProcess(decision, mi); + }); + } + + private Mono postProcess(AuthorizationDecision decision, MethodInvocation mi) { + return Mono.fromSupplier(() -> { + if (decision instanceof PostProcessableAuthorizationDecision postProcessableDecision) { + return postProcessableDecision.postProcess(); + } + return this.postProcessor.postProcessResult(mi, decision); + }).flatMap((result) -> { + if (Mono.class.isAssignableFrom(result.getClass())) { + return (Mono) result; + } + return Mono.justOrEmpty(result); + }); + } + private boolean isMultiValue(Class returnType, ReactiveAdapter adapter) { if (Flux.class.isAssignableFrom(returnType)) { return true; @@ -179,4 +218,14 @@ private static Object asFlow(Publisher publisher) { } + private static class ReactiveAuthorizationDeniedPostProcessorAdapter + implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) { + return Mono.error(new AuthorizationDeniedException("Access Denied", result)); + } + + } + } 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 1847ba3c1f5..9129a252ef3 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 @@ -19,6 +19,7 @@ import org.aopalliance.intercept.MethodInvocation; import reactor.core.publisher.Mono; +import org.springframework.context.ApplicationContext; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.prepost.PostAuthorize; @@ -61,6 +62,10 @@ public void setTemplateDefaults(PrePostTemplateDefaults defaults) { this.registry.setTemplateDefaults(defaults); } + public void setApplicationContext(ApplicationContext context) { + this.registry.setApplicationContext(context); + } + /** * Determines if an {@link Authentication} has access to the returned object from the * {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize} @@ -77,13 +82,14 @@ 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 ExpressionAttributeAuthorizationDecision(granted, attribute)); + .map((granted) -> new PostProcessableAuthorizationDecision<>(granted, postAuthorizeAttribute.getExpression(), result, postAuthorizeAttribute.getPostProcessor())); // @formatter:on } 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 383a0201130..4963d20f225 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 @@ -19,6 +19,7 @@ import org.aopalliance.intercept.MethodInvocation; import reactor.core.publisher.Mono; +import org.springframework.context.ApplicationContext; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.prepost.PreAuthorize; @@ -60,6 +61,10 @@ public void setTemplateDefaults(PrePostTemplateDefaults defaults) { this.registry.setTemplateDefaults(defaults); } + public void setApplicationContext(ApplicationContext context) { + this.registry.setApplicationContext(context); + } + /** * Determines if an {@link Authentication} has access to the {@link MethodInvocation} * by evaluating an expression from the {@link PreAuthorize} annotation. @@ -74,11 +79,12 @@ 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 ExpressionAttributeAuthorizationDecision(granted, attribute)); + .map((granted) -> new PostProcessableAuthorizationDecision<>(granted, preAuthorizeAttribute.getExpression(), mi, preAuthorizeAttribute.getPostProcessor())); // @formatter:on } 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 572fd754f4f..206ba592712 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 @@ -23,8 +23,12 @@ 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; @@ -66,14 +70,15 @@ public void invokeMonoWhenMockReactiveAuthorizationManagerThenVerify() throws Th given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( ReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty()); + given(mockReactiveAuthorizationManager.check(any(), any())) + .willReturn(Mono.just(new AuthorizationDecision(true))); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) .extracting(Mono::block) .isEqualTo("john"); - verify(mockReactiveAuthorizationManager).verify(any(), any()); + verify(mockReactiveAuthorizationManager).check(any(), any()); } @Test @@ -83,7 +88,8 @@ public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Th given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( ReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty()); + given(mockReactiveAuthorizationManager.check(any(), any())) + .willReturn(Mono.just(new AuthorizationDecision(true))); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); @@ -91,7 +97,7 @@ public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Th .extracting(Flux::collectList) .extracting(Mono::block, InstanceOfAssertFactories.list(String.class)) .containsExactly("john", "bob"); - verify(mockReactiveAuthorizationManager, times(2)).verify(any(), any()); + verify(mockReactiveAuthorizationManager, times(2)).check(any(), any()); } @Test @@ -101,8 +107,8 @@ public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedExce given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( ReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.verify(any(), any())) - .willReturn(Mono.error(new AccessDeniedException("Access Denied"))); + given(mockReactiveAuthorizationManager.check(any(), any())) + .willReturn(Mono.just(new AuthorizationDecision(false))); AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); @@ -110,7 +116,166 @@ public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedExce .isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) .extracting(Mono::block)) .withMessage("Access Denied"); - verify(mockReactiveAuthorizationManager).verify(any(), any()); + verify(mockReactiveAuthorizationManager).check(any(), any()); + } + + @Test + public void invokeFluxWhenAllValuesDeniedAndPostProcessorThenPostProcessorAppliedToEachValueEmitted() + throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); + given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + PostProcessableAuthorizationDecision decision = new PostProcessableAuthorizationDecision<>( + false, new LiteralExpression("1234"), new MethodInvocationResult(mockMethodInvocation, "john"), + new MaskingPostProcessor()); + given(mockReactiveAuthorizationManager.check(any(), any())).will((invocation) -> { + MethodInvocationResult mir = invocation.getArgument(1); + return Mono.just(createDecision(mir, new MaskingPostProcessor())); + }); + AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)) + .extracting(Flux::collectList) + .extracting(Mono::block, InstanceOfAssertFactories.list(String.class)) + .containsExactly("john-masked", "bob-masked"); + verify(mockReactiveAuthorizationManager, times(2)).check(any(), any()); + } + + @Test + public void invokeFluxWhenOneValueDeniedAndPostProcessorThenPostProcessorAppliedToDeniedValue() throws Throwable { + 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)); + } + return Mono.just(createDecision(argument, new MaskingPostProcessor())); + }); + AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)) + .extracting(Flux::collectList) + .extracting(Mono::block, InstanceOfAssertFactories.list(String.class)) + .containsExactly("john", "bob-masked"); + verify(mockReactiveAuthorizationManager, times(2)).check(any(), any()); + } + + @Test + public void invokeMonoWhenPostProcessableDecisionThenPostProcess() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + PostProcessableAuthorizationDecision decision = new PostProcessableAuthorizationDecision<>( + false, new LiteralExpression("1234"), new MethodInvocationResult(mockMethodInvocation, "john"), + new MaskingPostProcessor()); + given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision)); + AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .extracting(Mono::block) + .isEqualTo("john-masked"); + verify(mockReactiveAuthorizationManager).check(any(), any()); + } + + @Test + public void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsMonoThenPostProcessWorks() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + PostProcessableAuthorizationDecision decision = new PostProcessableAuthorizationDecision<>( + false, new LiteralExpression("1234"), new MethodInvocationResult(mockMethodInvocation, "john"), + new MonoMaskingPostProcessor()); + given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision)); + AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .extracting(Mono::block) + .isEqualTo("john-masked"); + verify(mockReactiveAuthorizationManager).check(any(), any()); + } + + @Test + public void invokeMonoWhenPostProcessableDecisionAndPostProcessResultIsNullThenPostProcessWorks() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + PostProcessableAuthorizationDecision decision = new PostProcessableAuthorizationDecision<>( + false, new LiteralExpression("1234"), new MethodInvocationResult(mockMethodInvocation, "john"), + new NullPostProcessor()); + given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.just(decision)); + AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .extracting(Mono::block) + .isEqualTo(null); + verify(mockReactiveAuthorizationManager).check(any(), any()); + } + + @Test + public void invokeMonoWhenEmptyDecisionThenUseDefaultPostProcessor() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + given(mockReactiveAuthorizationManager.check(any(), any())).willReturn(Mono.empty()); + AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThatExceptionOfType(AuthorizationDeniedException.class) + .isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .extracting(Mono::block)) + .withMessage("Access Denied"); + verify(mockReactiveAuthorizationManager).check(any(), any()); + } + + private PostProcessableAuthorizationDecision createDecision(MethodInvocationResult mir, + AuthorizationDeniedPostProcessor postProcessor) { + return new PostProcessableAuthorizationDecision<>(false, new LiteralExpression("1234"), mir, postProcessor); + } + + static class MaskingPostProcessor implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) { + return contextObject.getResult() + "-masked"; + } + + } + + static class MonoMaskingPostProcessor implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) { + return Mono.just(contextObject.getResult() + "-masked"); + } + + } + + static class NullPostProcessor implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) { + return null; + } + } class Sample { 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 13f6f405750..a89b993bfec 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,8 +23,12 @@ 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; @@ -67,14 +71,15 @@ public void invokeMonoWhenMockReactiveAuthorizationManagerThenVerify() throws Th given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( ReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); + given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))) + .willReturn(Mono.just(new AuthorizationDecision(true))); AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) .extracting(Mono::block) .isEqualTo("john"); - verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation)); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); } @Test @@ -84,7 +89,8 @@ public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Th given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( ReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); + given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))) + .willReturn(Mono.just(new AuthorizationDecision((true)))); AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); @@ -92,7 +98,7 @@ public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Th .extracting(Flux::collectList) .extracting(Mono::block, InstanceOfAssertFactories.list(String.class)) .containsExactly("john", "bob"); - verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation)); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); } @Test @@ -102,8 +108,8 @@ public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedExce given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( ReactiveAuthorizationManager.class); - given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))) - .willReturn(Mono.error(new AccessDeniedException("Access Denied"))); + given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))) + .willReturn(Mono.just(new AuthorizationDecision(false))); AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( Pointcut.TRUE, mockReactiveAuthorizationManager); Object result = interceptor.invoke(mockMethodInvocation); @@ -111,7 +117,119 @@ public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedExce .isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) .extracting(Mono::block)) .withMessage("Access Denied"); - verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation)); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); + } + + @Test + public void invokeMonoWhenDeniedAndPostProcessorThenInvokePostProcessor() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + PostProcessableAuthorizationDecision decision = new PostProcessableAuthorizationDecision<>( + false, new LiteralExpression("1234"), mockMethodInvocation, new MaskingPostProcessor()); + given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision)); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .extracting(Mono::block) + .isEqualTo("***"); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); + } + + @Test + public void invokeMonoWhenDeniedAndMonoPostProcessorThenInvokePostProcessor() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + PostProcessableAuthorizationDecision decision = new PostProcessableAuthorizationDecision<>( + false, new LiteralExpression("1234"), mockMethodInvocation, new MonoMaskingPostProcessor()); + given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision)); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .extracting(Mono::block) + .isEqualTo("***"); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); + } + + @Test + public void invokeFluxWhenDeniedAndPostProcessorThenInvokePostProcessor() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux"))); + given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + PostProcessableAuthorizationDecision decision = new PostProcessableAuthorizationDecision<>( + false, new LiteralExpression("1234"), mockMethodInvocation, new MonoMaskingPostProcessor()); + given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.just(decision)); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)) + .extracting(Flux::collectList) + .extracting(Mono::block, InstanceOfAssertFactories.list(String.class)) + .containsExactly("***"); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); + } + + @Test + public void invokeMonoWhenEmptyDecisionThenInvokeDefaultPostProcessor() throws Throwable { + MethodInvocation mockMethodInvocation = spy( + new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono"))); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + given(mockReactiveAuthorizationManager.check(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThatExceptionOfType(AuthorizationDeniedException.class) + .isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)) + .extracting(Mono::block)) + .withMessage("Access Denied"); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); + } + + @Test + public void invokeFluxWhenEmptyDecisionThenInvokeDefaultPostProcessor() throws Throwable { + 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(), eq(mockMethodInvocation))).willReturn(Mono.empty()); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThatExceptionOfType(AuthorizationDeniedException.class) + .isThrownBy(() -> assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)) + .extracting(Flux::blockFirst)) + .withMessage("Access Denied"); + verify(mockReactiveAuthorizationManager).check(any(), eq(mockMethodInvocation)); + } + + static class MaskingPostProcessor implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) { + return "***"; + } + + } + + static class MonoMaskingPostProcessor implements AuthorizationDeniedPostProcessor { + + @Override + public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) { + return Mono.just("***"); + } + } class Sample {