diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java index 8e129695c4c..24a500a36c6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.security.authorization.ReactiveAuthorizationManager; /** * @@ -69,4 +70,11 @@ */ int order() default Ordered.LOWEST_PRECEDENCE; + /** + * Indicate whether {@link ReactiveAuthorizationManager} based Method Security to be + * used. + * @since 5.8 + */ + boolean authorizationManager() default false; + } 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 new file mode 100644 index 00000000000..5ebb4f0944a --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationManagerMethodSecurityConfiguration.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2022 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.config.annotation.method.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; +import org.springframework.security.authorization.method.PostAuthorizeReactiveAuthorizationManager; +import org.springframework.security.authorization.method.PostFilterAuthorizationReactiveMethodInterceptor; +import org.springframework.security.authorization.method.PreAuthorizeReactiveAuthorizationManager; +import org.springframework.security.authorization.method.PreFilterAuthorizationReactiveMethodInterceptor; +import org.springframework.security.config.core.GrantedAuthorityDefaults; + +/** + * Configuration for a {@link ReactiveAuthenticationManager} based Method Security. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +@Configuration(proxyBeanMethods = false) +final class ReactiveAuthorizationManagerMethodSecurityConfiguration { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor( + MethodSecurityExpressionHandler expressionHandler) { + PreFilterAuthorizationReactiveMethodInterceptor preFilter = new PreFilterAuthorizationReactiveMethodInterceptor(); + preFilter.setExpressionHandler(expressionHandler); + return preFilter; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor( + MethodSecurityExpressionHandler expressionHandler) { + PreAuthorizeReactiveAuthorizationManager authorizationManager = new PreAuthorizeReactiveAuthorizationManager(); + authorizationManager.setExpressionHandler(expressionHandler); + return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor( + MethodSecurityExpressionHandler expressionHandler) { + PostFilterAuthorizationReactiveMethodInterceptor postFilter = new PostFilterAuthorizationReactiveMethodInterceptor(); + postFilter.setExpressionHandler(expressionHandler); + return postFilter; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor( + MethodSecurityExpressionHandler expressionHandler) { + PostAuthorizeReactiveAuthorizationManager authorizationManager = new PostAuthorizeReactiveAuthorizationManager(); + authorizationManager.setExpressionHandler(expressionHandler); + return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( + @Autowired(required = false) GrantedAuthorityDefaults grantedAuthorityDefaults) { + DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); + if (grantedAuthorityDefaults != null) { + handler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix()); + } + return handler; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java index 17e350e5f2f..fd0d304c359 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecuritySelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 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. @@ -17,37 +17,55 @@ package org.springframework.security.config.annotation.method.configuration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AdviceModeImportSelector; import org.springframework.context.annotation.AutoProxyRegistrar; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.NonNull; /** * @author Rob Winch + * @author Evgeniy Cheban * @since 5.0 */ -class ReactiveMethodSecuritySelector extends AdviceModeImportSelector { +class ReactiveMethodSecuritySelector implements ImportSelector { + + private final ImportSelector autoProxy = new AutoProxyRegistrarSelector(); @Override - protected String[] selectImports(AdviceMode adviceMode) { - if (adviceMode == AdviceMode.PROXY) { - return getProxyImports(); + public String[] selectImports(AnnotationMetadata importMetadata) { + if (!importMetadata.hasAnnotation(EnableReactiveMethodSecurity.class.getName())) { + return new String[0]; + } + EnableReactiveMethodSecurity annotation = importMetadata.getAnnotations() + .get(EnableReactiveMethodSecurity.class).synthesize(); + List imports = new ArrayList<>(Arrays.asList(this.autoProxy.selectImports(importMetadata))); + if (annotation.authorizationManager()) { + imports.add(ReactiveAuthorizationManagerMethodSecurityConfiguration.class.getName()); } - throw new IllegalStateException("AdviceMode " + adviceMode + " is not supported"); + else { + imports.add(ReactiveMethodSecurityConfiguration.class.getName()); + } + return imports.toArray(new String[0]); } - /** - * Return the imports to use if the {@link AdviceMode} is set to - * {@link AdviceMode#PROXY}. - *

- * Take care of adding the necessary JSR-107 import if it is available. - */ - private String[] getProxyImports() { - List result = new ArrayList<>(); - result.add(AutoProxyRegistrar.class.getName()); - result.add(ReactiveMethodSecurityConfiguration.class.getName()); - return result.toArray(new String[0]); + private static final class AutoProxyRegistrarSelector + extends AdviceModeImportSelector { + + private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() }; + + @Override + protected String[] selectImports(@NonNull AdviceMode adviceMode) { + if (adviceMode == AdviceMode.PROXY) { + return IMPORTS; + } + throw new IllegalStateException("AdviceMode " + adviceMode + " is not supported"); + } + } } 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 c206ef37f2a..9a9ff57da49 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 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. @@ -16,11 +16,14 @@ package org.springframework.security.config.annotation.method.configuration; +import reactor.core.publisher.Mono; + import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** * @author Rob Winch + * @author Evgeniy Cheban * @since 5.0 */ @Component @@ -34,6 +37,10 @@ public boolean check(long id) { return id % 2 == 0; } + public Mono checkReactive(long id) { + return Mono.defer(() -> Mono.just(id % 2 == 0)); + } + public boolean check(Authentication authentication, String message) { return message != null && message.contains(authentication.getName()); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java index 23d1df8069d..dd1d5765e6c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/DelegatingReactiveMessageService.java @@ -21,7 +21,9 @@ import reactor.core.publisher.Mono; import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.access.prepost.PreFilter; public class DelegatingReactiveMessageService implements ReactiveMessageService { @@ -60,6 +62,12 @@ public Mono monoPreAuthorizeBeanFindById(long id) { return this.delegate.monoPreAuthorizeBeanFindById(id); } + @Override + @PreAuthorize("@authz.checkReactive(#id)") + public Mono monoPreAuthorizeBeanFindByIdReactiveExpression(long id) { + return this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(id); + } + @Override @PostAuthorize("@authz.check(authentication, returnObject)") public Mono monoPostAuthorizeBeanFindById(long id) { @@ -95,6 +103,15 @@ public Flux fluxPostAuthorizeBeanFindById(long id) { return this.delegate.fluxPostAuthorizeBeanFindById(id); } + @PreFilter("filterObject.length > 3") + @PreAuthorize("hasRole('ADMIN')") + @PostFilter("filterObject.length > 5") + @PostAuthorize("returnObject == 'harold' or returnObject == 'jonathan'") + @Override + public Flux fluxManyAnnotations(Flux flux) { + return flux; + } + @Override public Publisher publisherFindById(long id) { return this.delegate.publisherFindById(id); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableAuthorizationManagerReactiveMethodSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableAuthorizationManagerReactiveMethodSecurityTests.java new file mode 100644 index 00000000000..3f21e1bdfb2 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/EnableAuthorizationManagerReactiveMethodSecurityTests.java @@ -0,0 +1,478 @@ +/* + * Copyright 2002-2022 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.config.annotation.method.configuration; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.util.context.Context; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; + +/** + * Tests for {@link EnableReactiveMethodSecurity} with the + * {@link EnableReactiveMethodSecurity#authorizationManager()} flag set to true. + * + * @author Evgeniy Cheban + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +public class EnableAuthorizationManagerReactiveMethodSecurityTests { + + @Autowired + ReactiveMessageService messageService; + + ReactiveMessageService delegate; + + TestPublisher result = TestPublisher.create(); + + Context withAdmin = ReactiveSecurityContextHolder + .withAuthentication(new TestingAuthenticationToken("admin", "password", "ROLE_USER", "ROLE_ADMIN")); + + Context withUser = ReactiveSecurityContextHolder + .withAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + + @AfterEach + public void cleanup() { + reset(this.delegate); + } + + @Autowired + public void setConfig(Config config) { + this.delegate = config.delegate; + } + + @Test + public void notPublisherPreAuthorizeFindByIdThenThrowsIllegalStateException() { + assertThatIllegalStateException().isThrownBy(() -> this.messageService.notPublisherPreAuthorizeFindById(1L)) + .withMessage("The returnType class java.lang.String on public abstract java.lang.String " + + "org.springframework.security.config.annotation.method.configuration.ReactiveMessageService" + + ".notPublisherPreAuthorizeFindById(long) must return an instance of org.reactivestreams" + + ".Publisher (i.e. Mono / Flux) or the function must be a Kotlin coroutine " + + "function in order to support Reactor Context"); + } + + @Test + public void monoWhenPermitAllThenAopDoesNotSubscribe() { + given(this.delegate.monoFindById(1L)).willReturn(Mono.from(this.result)); + this.delegate.monoFindById(1L); + this.result.assertNoSubscribers(); + } + + @Test + public void monoWhenPermitAllThenSuccess() { + given(this.delegate.monoFindById(1L)).willReturn(Mono.just("success")); + StepVerifier.create(this.delegate.monoFindById(1L)).expectNext("success").verifyComplete(); + } + + @Test + public void monoPreAuthorizeHasRoleWhenGrantedThenSuccess() { + given(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).willReturn(Mono.just("result")); + Mono findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L).contextWrite(this.withAdmin); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void monoPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() { + given(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).willReturn(Mono.from(this.result)); + Mono findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void monoPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() { + given(this.delegate.monoPreAuthorizeHasRoleFindById(1L)).willReturn(Mono.from(this.result)); + Mono findById = this.messageService.monoPreAuthorizeHasRoleFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void monoPreAuthorizeBeanWhenGrantedThenSuccess() { + given(this.delegate.monoPreAuthorizeBeanFindById(2L)).willReturn(Mono.just("result")); + Mono findById = this.messageService.monoPreAuthorizeBeanFindById(2L).contextWrite(this.withAdmin); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void monoPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() { + given(this.delegate.monoPreAuthorizeBeanFindById(2L)).willReturn(Mono.just("result")); + Mono findById = this.messageService.monoPreAuthorizeBeanFindById(2L); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void monoPreAuthorizeBeanWhenNoAuthenticationThenDenied() { + given(this.delegate.monoPreAuthorizeBeanFindById(1L)).willReturn(Mono.from(this.result)); + Mono findById = this.messageService.monoPreAuthorizeBeanFindById(1L); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void monoPreAuthorizeBeanWhenNotAuthorizedThenDenied() { + given(this.delegate.monoPreAuthorizeBeanFindById(1L)).willReturn(Mono.from(this.result)); + Mono findById = this.messageService.monoPreAuthorizeBeanFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void monoPreAuthorizeBeanReactiveExpressionWhenGrantedThenSuccess() { + given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(2L)).willReturn(Mono.just("result")); + Mono findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(2L) + .contextWrite(this.withAdmin); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void monoPreAuthorizeBeanReactiveExpressionWhenNotAuthenticatedAndGrantedThenSuccess() { + given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(2L)).willReturn(Mono.just("result")); + Mono findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(2L); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void monoPreAuthorizeBeanReactiveExpressionWhenNoAuthenticationThenDenied() { + given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(1L)).willReturn(Mono.from(this.result)); + Mono findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(1L); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void monoPreAuthorizeBeanReactiveExpressionWhenNotAuthorizedThenDenied() { + given(this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(1L)).willReturn(Mono.from(this.result)); + Mono findById = this.messageService.monoPreAuthorizeBeanFindByIdReactiveExpression(1L) + .contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void monoPostAuthorizeWhenAuthorizedThenSuccess() { + given(this.delegate.monoPostAuthorizeFindById(1L)).willReturn(Mono.just("user")); + Mono findById = this.messageService.monoPostAuthorizeFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectNext("user").verifyComplete(); + } + + @Test + public void monoPostAuthorizeWhenNotAuthorizedThenDenied() { + given(this.delegate.monoPostAuthorizeBeanFindById(1L)).willReturn(Mono.just("not-authorized")); + Mono findById = this.messageService.monoPostAuthorizeBeanFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + } + + @Test + public void monoPostAuthorizeWhenBeanAndAuthorizedThenSuccess() { + given(this.delegate.monoPostAuthorizeBeanFindById(2L)).willReturn(Mono.just("user")); + Mono findById = this.messageService.monoPostAuthorizeBeanFindById(2L).contextWrite(this.withUser); + StepVerifier.create(findById).expectNext("user").verifyComplete(); + } + + @Test + public void monoPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() { + given(this.delegate.monoPostAuthorizeBeanFindById(2L)).willReturn(Mono.just("anonymous")); + Mono findById = this.messageService.monoPostAuthorizeBeanFindById(2L); + StepVerifier.create(findById).expectNext("anonymous").verifyComplete(); + } + + @Test + public void monoPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() { + given(this.delegate.monoPostAuthorizeBeanFindById(1L)).willReturn(Mono.just("not-authorized")); + Mono findById = this.messageService.monoPostAuthorizeBeanFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + } + + // Flux tests + @Test + public void fluxWhenPermitAllThenAopDoesNotSubscribe() { + given(this.delegate.fluxFindById(1L)).willReturn(Flux.from(this.result)); + this.delegate.fluxFindById(1L); + this.result.assertNoSubscribers(); + } + + @Test + public void fluxWhenPermitAllThenSuccess() { + given(this.delegate.fluxFindById(1L)).willReturn(Flux.just("success")); + StepVerifier.create(this.delegate.fluxFindById(1L)).expectNext("success").verifyComplete(); + } + + @Test + public void fluxPreAuthorizeHasRoleWhenGrantedThenSuccess() { + given(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).willReturn(Flux.just("result")); + Flux findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L).contextWrite(this.withAdmin); + StepVerifier.create(findById).consumeNextWith((s) -> assertThat(s).isEqualTo("result")).verifyComplete(); + } + + @Test + public void fluxPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() { + given(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).willReturn(Flux.from(this.result)); + Flux findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void fluxPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() { + given(this.delegate.fluxPreAuthorizeHasRoleFindById(1L)).willReturn(Flux.from(this.result)); + Flux findById = this.messageService.fluxPreAuthorizeHasRoleFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void fluxPreAuthorizeBeanWhenGrantedThenSuccess() { + given(this.delegate.fluxPreAuthorizeBeanFindById(2L)).willReturn(Flux.just("result")); + Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(2L).contextWrite(this.withAdmin); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void fluxPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() { + given(this.delegate.fluxPreAuthorizeBeanFindById(2L)).willReturn(Flux.just("result")); + Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(2L); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void fluxPreAuthorizeBeanWhenNoAuthenticationThenDenied() { + given(this.delegate.fluxPreAuthorizeBeanFindById(1L)).willReturn(Flux.from(this.result)); + Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(1L); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void fluxPreAuthorizeBeanWhenNotAuthorizedThenDenied() { + given(this.delegate.fluxPreAuthorizeBeanFindById(1L)).willReturn(Flux.from(this.result)); + Flux findById = this.messageService.fluxPreAuthorizeBeanFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void fluxPostAuthorizeWhenAuthorizedThenSuccess() { + given(this.delegate.fluxPostAuthorizeFindById(1L)).willReturn(Flux.just("user")); + Flux findById = this.messageService.fluxPostAuthorizeFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectNext("user").verifyComplete(); + } + + @Test + public void fluxPostAuthorizeWhenNotAuthorizedThenDenied() { + given(this.delegate.fluxPostAuthorizeBeanFindById(1L)).willReturn(Flux.just("not-authorized")); + Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + } + + @Test + public void fluxPostAuthorizeWhenBeanAndAuthorizedThenSuccess() { + given(this.delegate.fluxPostAuthorizeBeanFindById(2L)).willReturn(Flux.just("user")); + Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(2L).contextWrite(this.withUser); + StepVerifier.create(findById).expectNext("user").verifyComplete(); + } + + @Test + public void fluxPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() { + given(this.delegate.fluxPostAuthorizeBeanFindById(2L)).willReturn(Flux.just("anonymous")); + Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(2L); + StepVerifier.create(findById).expectNext("anonymous").verifyComplete(); + } + + @Test + public void fluxPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() { + given(this.delegate.fluxPostAuthorizeBeanFindById(1L)).willReturn(Flux.just("not-authorized")); + Flux findById = this.messageService.fluxPostAuthorizeBeanFindById(1L).contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + } + + @Test + public void fluxManyAnnotationsWhenMeetsConditionsThenReturnsFilteredFlux() { + Flux flux = this.messageService.fluxManyAnnotations(Flux.just("harold", "jonathan", "pete", "bo")) + .contextWrite(this.withAdmin); + StepVerifier.create(flux).expectNext("harold", "jonathan").verifyComplete(); + } + + @Test + public void fluxManyAnnotationsWhenUserThenFails() { + Flux flux = this.messageService.fluxManyAnnotations(Flux.just("harold", "jonathan", "pete", "bo")) + .contextWrite(this.withUser); + StepVerifier.create(flux).expectError(AccessDeniedException.class).verify(); + } + + @Test + public void fluxManyAnnotationsWhenNameNotAllowedThenFails() { + Flux flux = this.messageService + .fluxManyAnnotations(Flux.just("harold", "jonathan", "michael", "pete", "bo")) + .contextWrite(this.withAdmin); + StepVerifier.create(flux).expectNext("harold", "jonathan").expectError(AccessDeniedException.class).verify(); + } + + // Publisher tests + @Test + public void publisherWhenPermitAllThenAopDoesNotSubscribe() { + given(this.delegate.publisherFindById(1L)).willReturn(this.result); + this.delegate.publisherFindById(1L); + this.result.assertNoSubscribers(); + } + + @Test + public void publisherWhenPermitAllThenSuccess() { + given(this.delegate.publisherFindById(1L)).willReturn(publisherJust("success")); + StepVerifier.create(this.delegate.publisherFindById(1L)).expectNext("success").verifyComplete(); + } + + @Test + public void publisherPreAuthorizeHasRoleWhenGrantedThenSuccess() { + given(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).willReturn(publisherJust("result")); + Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeHasRoleFindById(1L)) + .contextWrite(this.withAdmin); + StepVerifier.create(findById).consumeNextWith((s) -> assertThat(s).isEqualTo("result")).verifyComplete(); + } + + @Test + public void publisherPreAuthorizeHasRoleWhenNoAuthenticationThenDenied() { + given(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).willReturn(this.result); + Publisher findById = this.messageService.publisherPreAuthorizeHasRoleFindById(1L); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void publisherPreAuthorizeHasRoleWhenNotAuthorizedThenDenied() { + given(this.delegate.publisherPreAuthorizeHasRoleFindById(1L)).willReturn(this.result); + Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeHasRoleFindById(1L)) + .contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void publisherPreAuthorizeBeanWhenGrantedThenSuccess() { + given(this.delegate.publisherPreAuthorizeBeanFindById(2L)).willReturn(publisherJust("result")); + Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeBeanFindById(2L)) + .contextWrite(this.withAdmin); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void publisherPreAuthorizeBeanWhenNotAuthenticatedAndGrantedThenSuccess() { + given(this.delegate.publisherPreAuthorizeBeanFindById(2L)).willReturn(publisherJust("result")); + Publisher findById = this.messageService.publisherPreAuthorizeBeanFindById(2L); + StepVerifier.create(findById).expectNext("result").verifyComplete(); + } + + @Test + public void publisherPreAuthorizeBeanWhenNoAuthenticationThenDenied() { + given(this.delegate.publisherPreAuthorizeBeanFindById(1L)).willReturn(this.result); + Publisher findById = this.messageService.publisherPreAuthorizeBeanFindById(1L); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void publisherPreAuthorizeBeanWhenNotAuthorizedThenDenied() { + given(this.delegate.publisherPreAuthorizeBeanFindById(1L)).willReturn(this.result); + Publisher findById = Flux.from(this.messageService.publisherPreAuthorizeBeanFindById(1L)) + .contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + this.result.assertNoSubscribers(); + } + + @Test + public void publisherPostAuthorizeWhenAuthorizedThenSuccess() { + given(this.delegate.publisherPostAuthorizeFindById(1L)).willReturn(publisherJust("user")); + Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeFindById(1L)) + .contextWrite(this.withUser); + StepVerifier.create(findById).expectNext("user").verifyComplete(); + } + + @Test + public void publisherPostAuthorizeWhenNotAuthorizedThenDenied() { + given(this.delegate.publisherPostAuthorizeBeanFindById(1L)).willReturn(publisherJust("not-authorized")); + Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L)) + .contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + } + + @Test + public void publisherPostAuthorizeWhenBeanAndAuthorizedThenSuccess() { + given(this.delegate.publisherPostAuthorizeBeanFindById(2L)).willReturn(publisherJust("user")); + Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(2L)) + .contextWrite(this.withUser); + StepVerifier.create(findById).expectNext("user").verifyComplete(); + } + + @Test + public void publisherPostAuthorizeWhenBeanAndNotAuthenticatedAndAuthorizedThenSuccess() { + given(this.delegate.publisherPostAuthorizeBeanFindById(2L)).willReturn(publisherJust("anonymous")); + Publisher findById = this.messageService.publisherPostAuthorizeBeanFindById(2L); + StepVerifier.create(findById).expectNext("anonymous").verifyComplete(); + } + + @Test + public void publisherPostAuthorizeWhenBeanAndNotAuthorizedThenDenied() { + given(this.delegate.publisherPostAuthorizeBeanFindById(1L)).willReturn(publisherJust("not-authorized")); + Publisher findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L)) + .contextWrite(this.withUser); + StepVerifier.create(findById).expectError(AccessDeniedException.class).verify(); + } + + static Publisher publisher(Flux flux) { + return (subscriber) -> flux.subscribe(subscriber); + } + + static Publisher publisherJust(T... data) { + return publisher(Flux.just(data)); + } + + @EnableReactiveMethodSecurity(authorizationManager = true) + static class Config { + + ReactiveMessageService delegate = mock(ReactiveMessageService.class); + + @Bean + DelegatingReactiveMessageService defaultMessageService() { + return new DelegatingReactiveMessageService(this.delegate); + } + + @Bean + Authz authz() { + return new Authz(); + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java index 908014c65c3..63ea868cd24 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/ReactiveMessageService.java @@ -32,6 +32,8 @@ public interface ReactiveMessageService { Mono monoPreAuthorizeBeanFindById(long id); + Mono monoPreAuthorizeBeanFindByIdReactiveExpression(long id); + Mono monoPostAuthorizeBeanFindById(long id); Flux fluxFindById(long id); @@ -44,6 +46,8 @@ public interface ReactiveMessageService { Flux fluxPostAuthorizeBeanFindById(long id); + Flux fluxManyAnnotations(Flux flux); + Publisher publisherFindById(long id); Publisher publisherPreAuthorizeHasRoleFindById(long id); diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAfterReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAfterReactiveMethodInterceptor.java new file mode 100644 index 00000000000..ae43393e52b --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationAfterReactiveMethodInterceptor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2022 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 java.lang.reflect.Method; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.core.Ordered; + +/** + * A {@link MethodInterceptor} that wraps a {@link Mono} or a {@link Flux} using + * deffer call. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class AuthorizationAfterReactiveMethodInterceptor + implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { + + private final Pointcut pointcut = AuthorizationMethodPointcuts.forAllAnnotations(); + + private final int order = AuthorizationInterceptorsOrder.LAST.getOrder(); + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Method method = mi.getMethod(); + Class returnType = method.getReturnType(); + if (Mono.class.isAssignableFrom(returnType)) { + return Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)); + } + return Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi)); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public int getOrder() { + return this.order; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeanFactoryPostProcessor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeanFactoryPostProcessor.java new file mode 100644 index 00000000000..b622562c63a --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeanFactoryPostProcessor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2022 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.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; + +/** + * Adds {@link AuthorizationBeforeReactiveMethodInterceptor} and + * {@link AuthorizationAfterReactiveMethodInterceptor} bean definitions to the + * {@link BeanDefinitionRegistry} if they have not already been added. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class AuthorizationBeanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor { + + private static final String BEFORE_INTERCEPTOR_BEAN_NAME = "org.springframework.security.authorization.method.authorizationBeforeReactiveMethodInterceptor"; + + private static final String AFTER_INTERCEPTOR_BEAN_NAME = "org.springframework.security.authorization.method.authorizationAfterReactiveMethodInterceptor"; + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(BEFORE_INTERCEPTOR_BEAN_NAME)) { + RootBeanDefinition beforeInterceptor = new RootBeanDefinition( + AuthorizationBeforeReactiveMethodInterceptor.class); + beforeInterceptor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(BEFORE_INTERCEPTOR_BEAN_NAME, beforeInterceptor); + } + if (!registry.containsBeanDefinition(AFTER_INTERCEPTOR_BEAN_NAME)) { + RootBeanDefinition afterInterceptor = new RootBeanDefinition( + AuthorizationAfterReactiveMethodInterceptor.class); + afterInterceptor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AFTER_INTERCEPTOR_BEAN_NAME, afterInterceptor); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeforeReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeforeReactiveMethodInterceptor.java new file mode 100644 index 00000000000..2462fb782fd --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationBeforeReactiveMethodInterceptor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2022 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 java.lang.reflect.Method; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.reactivestreams.Publisher; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.core.Ordered; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.util.Assert; + +/** + * A {@link MethodInterceptor} which validates and transforms the return type for methods + * that return a {@link Publisher}. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class AuthorizationBeforeReactiveMethodInterceptor + implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { + + private final Pointcut pointcut = AuthorizationMethodPointcuts.forAllAnnotations(); + + private final int order = AuthorizationInterceptorsOrder.FIRST.getOrder(); + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Method method = mi.getMethod(); + Class returnType = method.getReturnType(); + Assert.state(Publisher.class.isAssignableFrom(returnType), + () -> "The returnType " + returnType + " on " + method + + " must return an instance of org.reactivestreams.Publisher " + + "(i.e. Mono / Flux) or the function must be a Kotlin coroutine " + + "function in order to support Reactor Context"); + Publisher publisher = ReactiveMethodInvocationUtils.proceed(mi); + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnType); + return (adapter != null) ? adapter.fromPublisher(publisher) : publisher; + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public int getOrder() { + return this.order; + } + +} 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 new file mode 100644 index 00000000000..91010d85c46 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptor.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2022 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.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * A {@link MethodInterceptor} which can determine if an {@link Authentication} has access + * to the returned object from the {@link MethodInvocation} using the configured + * {@link ReactiveAuthorizationManager}. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class AuthorizationManagerAfterReactiveMethodInterceptor implements Ordered, MethodInterceptor, + PointcutAdvisor, AopInfrastructureBean, BeanDefinitionRegistryPostProcessor { + + private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor(); + + private final Pointcut pointcut; + + private final ReactiveAuthorizationManager authorizationManager; + + private int order = AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder(); + + /** + * Creates an instance for the {@link PostAuthorize} annotation. + * @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use + */ + public static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorize() { + return postAuthorize(new PostAuthorizeReactiveAuthorizationManager()); + } + + /** + * Creates an instance for the {@link PostAuthorize} annotation. + * @param authorizationManager the {@link ReactiveAuthorizationManager} to use + * @return the {@link AuthorizationManagerAfterReactiveMethodInterceptor} to use + */ + public static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorize( + ReactiveAuthorizationManager authorizationManager) { + return new AuthorizationManagerAfterReactiveMethodInterceptor( + AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager); + } + + /** + * Creates an instance. + * @param pointcut the {@link Pointcut} to use + * @param authorizationManager the {@link ReactiveAuthorizationManager} to use + */ + public AuthorizationManagerAfterReactiveMethodInterceptor(Pointcut pointcut, + ReactiveAuthorizationManager authorizationManager) { + Assert.notNull(pointcut, "pointcut cannot be null"); + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.pointcut = pointcut; + this.authorizationManager = authorizationManager; + } + + /** + * Determines if an {@link Authentication} has access to the returned object from the + * {@link MethodInvocation} using the configured {@link ReactiveAuthorizationManager}. + * @param mi the {@link MethodInvocation} to use + * @return the {@link Publisher} from the {@link MethodInvocation} or a + * {@link Publisher} error if access is denied + */ + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Publisher publisher = ReactiveMethodInvocationUtils.proceed(mi); + Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); + if (publisher instanceof Mono) { + Mono mono = (Mono) publisher; + return mono.flatMap((result) -> postAuthorize(authentication, mi, result)); + } + return Flux.from(publisher).flatMap((result) -> postAuthorize(authentication, mi, result)); + } + + private Mono postAuthorize(Mono authentication, MethodInvocation mi, Object result) { + return this.authorizationManager.verify(authentication, new MethodInvocationResult(mi, result)) + .thenReturn(result); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory); + } + +} 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 new file mode 100644 index 00000000000..c0e213cd09e --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptor.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2022 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.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +/** + * A {@link MethodInterceptor} which can determine if an {@link Authentication} has access + * to the {@link MethodInvocation} using the configured + * {@link ReactiveAuthorizationManager}. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class AuthorizationManagerBeforeReactiveMethodInterceptor implements Ordered, MethodInterceptor, + PointcutAdvisor, AopInfrastructureBean, BeanDefinitionRegistryPostProcessor { + + private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor(); + + private final Pointcut pointcut; + + private final ReactiveAuthorizationManager authorizationManager; + + private int order = AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder(); + + /** + * Creates an instance for the {@link PreAuthorize} annotation. + * @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use + */ + public static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorize() { + return preAuthorize(new PreAuthorizeReactiveAuthorizationManager()); + } + + /** + * Creates an instance for the {@link PreAuthorize} annotation. + * @param authorizationManager the {@link ReactiveAuthorizationManager} to use + * @return the {@link AuthorizationManagerBeforeReactiveMethodInterceptor} to use + */ + public static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorize( + ReactiveAuthorizationManager authorizationManager) { + return new AuthorizationManagerBeforeReactiveMethodInterceptor( + AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager); + } + + /** + * Creates an instance. + * @param pointcut the {@link Pointcut} to use + * @param authorizationManager the {@link ReactiveAuthorizationManager} to use + */ + public AuthorizationManagerBeforeReactiveMethodInterceptor(Pointcut pointcut, + ReactiveAuthorizationManager authorizationManager) { + Assert.notNull(pointcut, "pointcut cannot be null"); + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.pointcut = pointcut; + this.authorizationManager = authorizationManager; + } + + /** + * Determines if an {@link Authentication} has access to the {@link MethodInvocation} + * using the configured {@link ReactiveAuthorizationManager}. + * @param mi the {@link MethodInvocation} to use + * @return the {@link Publisher} from the {@link MethodInvocation} or a + * {@link Publisher} error if access is denied + */ + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Publisher publisher = ReactiveMethodInvocationUtils.proceed(mi); + Mono authentication = ReactiveAuthenticationUtils.getAuthentication(); + Mono preAuthorize = this.authorizationManager.verify(authentication, mi); + if (publisher instanceof Mono) { + return preAuthorize.then((Mono) publisher); + } + return preAuthorize.thenMany(publisher); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java index e764d95d838..cbdba35f3a6 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AuthorizationMethodPointcuts.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -22,12 +22,21 @@ import org.springframework.aop.support.ComposablePointcut; import org.springframework.aop.support.Pointcuts; import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.access.prepost.PreFilter; /** * @author Josh Cummings + * @author Evgeniy Cheban */ final class AuthorizationMethodPointcuts { + static Pointcut forAllAnnotations() { + return forAnnotations(PreFilter.class, PreAuthorize.class, PostFilter.class, PostAuthorize.class); + } + @SafeVarargs static Pointcut forAnnotations(Class... annotations) { ComposablePointcut pointcut = null; 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 3d605f08531..d5180115088 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 @@ -16,23 +16,17 @@ package org.springframework.security.authorization.method; -import java.lang.reflect.Method; import java.util.function.Supplier; import org.aopalliance.intercept.MethodInvocation; -import reactor.util.annotation.NonNull; -import org.springframework.aop.support.AopUtils; import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; import org.springframework.security.access.expression.ExpressionUtils; -import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; 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.AuthorizationManager; import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; /** * An {@link AuthorizationManager} which can determine if an {@link Authentication} may @@ -46,15 +40,12 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry(); - private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - /** * Use this the {@link MethodSecurityExpressionHandler}. * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use */ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { - Assert.notNull(expressionHandler, "expressionHandler cannot be null"); - this.expressionHandler = expressionHandler; + this.registry.setExpressionHandler(expressionHandler); } /** @@ -72,36 +63,11 @@ public AuthorizationDecision check(Supplier authentication, Meth if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return null; } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, - mi.getMethodInvocation()); - this.expressionHandler.setReturnObject(mi.getResult(), ctx); + MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); + EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation()); + expressionHandler.setReturnObject(mi.getResult(), ctx); boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); return new ExpressionAttributeAuthorizationDecision(granted, attribute); } - private final class PostAuthorizeExpressionAttributeRegistry - extends AbstractExpressionAttributeRegistry { - - @NonNull - @Override - ExpressionAttribute resolveAttribute(Method method, Class targetClass) { - Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod); - if (postAuthorize == null) { - return ExpressionAttribute.NULL_ATTRIBUTE; - } - Expression postAuthorizeExpression = PostAuthorizeAuthorizationManager.this.expressionHandler - .getExpressionParser().parseExpression(postAuthorize.value()); - return new ExpressionAttribute(postAuthorizeExpression); - } - - private PostAuthorize findPostAuthorizeAnnotation(Method method) { - PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, - PostAuthorize.class); - return (postAuthorize != null) ? postAuthorize : AuthorizationAnnotationUtils - .findUniqueAnnotation(method.getDeclaringClass(), PostAuthorize.class); - } - - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java new file mode 100644 index 00000000000..7ea4e3c9c44 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2022 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 java.lang.reflect.Method; + +import reactor.util.annotation.NonNull; + +import org.springframework.aop.support.AopUtils; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.util.Assert; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + MethodSecurityExpressionHandler getExpressionHandler() { + return this.expressionHandler; + } + + void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + @NonNull + @Override + ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PostAuthorize postAuthorize = findPostAuthorizeAnnotation(specificMethod); + if (postAuthorize == null) { + return ExpressionAttribute.NULL_ATTRIBUTE; + } + Expression postAuthorizeExpression = this.expressionHandler.getExpressionParser() + .parseExpression(postAuthorize.value()); + return new ExpressionAttribute(postAuthorizeExpression); + } + + private PostAuthorize findPostAuthorizeAnnotation(Method method) { + PostAuthorize postAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostAuthorize.class); + return (postAuthorize != null) ? postAuthorize + : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostAuthorize.class); + } + +} 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 new file mode 100644 index 00000000000..ad34c3d009d --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManager.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2022 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 reactor.core.publisher.Mono; + +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.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; + +/** + * A {@link ReactiveAuthorizationManager} which can determine if an {@link Authentication} + * has access to the returned object from the {@link MethodInvocation} by evaluating an + * expression from the {@link PostAuthorize} annotation. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class PostAuthorizeReactiveAuthorizationManager + implements ReactiveAuthorizationManager { + + private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry(); + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + this.registry.setExpressionHandler(expressionHandler); + } + + /** + * Determines if an {@link Authentication} has access to the returned object from the + * {@link MethodInvocation} by evaluating an expression from the {@link PostAuthorize} + * annotation. + * @param authentication the {@link Mono} of the {@link Authentication} to check + * @param result the {@link MethodInvocationResult} to check + * @return a Mono of the {@link AuthorizationDecision} or an empty {@link Mono} if the + * {@link PostAuthorize} annotation is not present + */ + @Override + public Mono check(Mono authentication, MethodInvocationResult result) { + MethodInvocation mi = result.getMethodInvocation(); + ExpressionAttribute attribute = this.registry.getAttribute(mi); + if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + return Mono.empty(); + } + 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)); + // @formatter:on + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java index 7f931a39876..8843b4345fa 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java @@ -16,7 +16,6 @@ package org.springframework.security.authorization.method; -import java.lang.reflect.Method; import java.util.function.Supplier; import org.aopalliance.aop.Advice; @@ -26,19 +25,14 @@ import org.springframework.aop.Pointcut; import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.aop.support.AopUtils; import org.springframework.core.Ordered; import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.lang.NonNull; -import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.util.Assert; /** * A {@link MethodInterceptor} which filters a {@code returnedObject} from the @@ -61,8 +55,6 @@ public final class PostFilterAuthorizationMethodInterceptor private final Pointcut pointcut; - private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - /** * Creates a {@link PostFilterAuthorizationMethodInterceptor} using the provided * parameters @@ -76,8 +68,7 @@ public PostFilterAuthorizationMethodInterceptor() { * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use */ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { - Assert.notNull(expressionHandler, "expressionHandler cannot be null"); - this.expressionHandler = expressionHandler; + this.registry.setExpressionHandler(expressionHandler); } /** @@ -133,8 +124,9 @@ public Object invoke(MethodInvocation mi) throws Throwable { if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return returnedObject; } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(this.authentication, mi); - return this.expressionHandler.filter(returnedObject, attribute.getExpression(), ctx); + MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); + EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi); + return expressionHandler.filter(returnedObject, attribute.getExpression(), ctx); } private Supplier getAuthentication(SecurityContextHolderStrategy strategy) { @@ -148,28 +140,4 @@ private Supplier getAuthentication(SecurityContextHolderStrategy }; } - private final class PostFilterExpressionAttributeRegistry - extends AbstractExpressionAttributeRegistry { - - @NonNull - @Override - ExpressionAttribute resolveAttribute(Method method, Class targetClass) { - Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PostFilter postFilter = findPostFilterAnnotation(specificMethod); - if (postFilter == null) { - return ExpressionAttribute.NULL_ATTRIBUTE; - } - Expression postFilterExpression = PostFilterAuthorizationMethodInterceptor.this.expressionHandler - .getExpressionParser().parseExpression(postFilter.value()); - return new ExpressionAttribute(postFilterExpression); - } - - private PostFilter findPostFilterAnnotation(Method method) { - PostFilter postFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostFilter.class); - return (postFilter != null) ? postFilter - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostFilter.class); - } - - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java new file mode 100644 index 00000000000..601cb10a2e7 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptor.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2022 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.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.expression.EvaluationContext; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.access.prepost.PostFilter; + +/** + * A {@link MethodInterceptor} which filters the returned object from the + * {@link MethodInvocation} by evaluating an expression from the {@link PostFilter} + * annotation. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class PostFilterAuthorizationReactiveMethodInterceptor implements Ordered, MethodInterceptor, + PointcutAdvisor, AopInfrastructureBean, BeanDefinitionRegistryPostProcessor { + + private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor(); + + private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry(); + + private final Pointcut pointcut; + + private int order = AuthorizationInterceptorsOrder.POST_FILTER.getOrder(); + + /** + * Creates an instance. + */ + public PostFilterAuthorizationReactiveMethodInterceptor() { + this.pointcut = AuthorizationMethodPointcuts.forAnnotations(PostFilter.class); + } + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + this.registry.setExpressionHandler(expressionHandler); + } + + /** + * Filters the returned object from the {@link MethodInvocation} by evaluating an + * expression from the {@link PostFilter} annotation. + * @param mi the {@link MethodInvocation} to use + * @return the {@link Publisher} to use + */ + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Publisher publisher = ReactiveMethodInvocationUtils.proceed(mi); + ExpressionAttribute attribute = this.registry.getAttribute(mi); + if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + return publisher; + } + Mono toInvoke = ReactiveAuthenticationUtils.getAuthentication() + .map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi)); + if (publisher instanceof Mono) { + return toInvoke.flatMap((ctx) -> filterMono((Mono) publisher, ctx, attribute)); + } + return toInvoke.flatMapMany((ctx) -> filterPublisher(publisher, ctx, attribute)); + } + + private Mono filterMono(Mono mono, EvaluationContext ctx, ExpressionAttribute attribute) { + return mono.doOnNext((result) -> setFilterObject(ctx, result)) + .flatMap((result) -> postFilter(ctx, result, attribute)); + } + + private Flux filterPublisher(Publisher publisher, EvaluationContext ctx, ExpressionAttribute attribute) { + return Flux.from(publisher).doOnNext((result) -> setFilterObject(ctx, result)) + .flatMap((result) -> postFilter(ctx, result, attribute)); + } + + private void setFilterObject(EvaluationContext ctx, Object result) { + ((MethodSecurityExpressionOperations) ctx.getRootObject().getValue()).setFilterObject(result); + } + + private Mono postFilter(EvaluationContext ctx, Object result, ExpressionAttribute attribute) { + return ReactiveExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx) + .flatMap((granted) -> granted ? Mono.just(result) : Mono.empty()); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory); + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java new file mode 100644 index 00000000000..21dff773646 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2022 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 java.lang.reflect.Method; + +import org.springframework.aop.support.AopUtils; +import org.springframework.expression.Expression; +import org.springframework.lang.NonNull; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.util.Assert; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class PostFilterExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + MethodSecurityExpressionHandler getExpressionHandler() { + return this.expressionHandler; + } + + void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + @NonNull + @Override + ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PostFilter postFilter = findPostFilterAnnotation(specificMethod); + if (postFilter == null) { + return ExpressionAttribute.NULL_ATTRIBUTE; + } + Expression postFilterExpression = this.expressionHandler.getExpressionParser() + .parseExpression(postFilter.value()); + return new ExpressionAttribute(postFilterExpression); + } + + private PostFilter findPostFilterAnnotation(Method method) { + PostFilter postFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PostFilter.class); + return (postFilter != null) ? postFilter + : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PostFilter.class); + } + +} 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 cbf861a2bb3..04183cc67c6 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 @@ -16,23 +16,17 @@ package org.springframework.security.authorization.method; -import java.lang.reflect.Method; import java.util.function.Supplier; import org.aopalliance.intercept.MethodInvocation; -import reactor.util.annotation.NonNull; -import org.springframework.aop.support.AopUtils; import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; import org.springframework.security.access.expression.ExpressionUtils; -import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; 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.AuthorizationManager; import org.springframework.security.core.Authentication; -import org.springframework.util.Assert; /** * An {@link AuthorizationManager} which can determine if an {@link Authentication} may @@ -46,15 +40,12 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry(); - private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - /** * Sets the {@link MethodSecurityExpressionHandler}. * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use */ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { - Assert.notNull(expressionHandler, "expressionHandler cannot be null"); - this.expressionHandler = expressionHandler; + this.registry.setExpressionHandler(expressionHandler); } /** @@ -72,33 +63,9 @@ public AuthorizationDecision check(Supplier authentication, Meth if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { return null; } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi); + EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi); boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx); return new ExpressionAttributeAuthorizationDecision(granted, attribute); } - private final class PreAuthorizeExpressionAttributeRegistry - extends AbstractExpressionAttributeRegistry { - - @NonNull - @Override - ExpressionAttribute resolveAttribute(Method method, Class targetClass) { - Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod); - if (preAuthorize == null) { - return ExpressionAttribute.NULL_ATTRIBUTE; - } - Expression preAuthorizeExpression = PreAuthorizeAuthorizationManager.this.expressionHandler - .getExpressionParser().parseExpression(preAuthorize.value()); - return new ExpressionAttribute(preAuthorizeExpression); - } - - private PreAuthorize findPreAuthorizeAnnotation(Method method) { - PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class); - return (preAuthorize != null) ? preAuthorize - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreAuthorize.class); - } - - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java new file mode 100644 index 00000000000..62bf863cd7d --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2022 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 java.lang.reflect.Method; + +import reactor.util.annotation.NonNull; + +import org.springframework.aop.support.AopUtils; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.Assert; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry { + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + /** + * Returns the {@link MethodSecurityExpressionHandler}. + * @return the {@link MethodSecurityExpressionHandler} to use + */ + MethodSecurityExpressionHandler getExpressionHandler() { + return this.expressionHandler; + } + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + @NonNull + @Override + ExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod); + if (preAuthorize == null) { + return ExpressionAttribute.NULL_ATTRIBUTE; + } + Expression preAuthorizeExpression = this.expressionHandler.getExpressionParser() + .parseExpression(preAuthorize.value()); + return new ExpressionAttribute(preAuthorizeExpression); + } + + private PreAuthorize findPreAuthorizeAnnotation(Method method) { + PreAuthorize preAuthorize = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreAuthorize.class); + return (preAuthorize != null) ? preAuthorize + : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreAuthorize.class); + } + +} 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 new file mode 100644 index 00000000000..7ff923300a3 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManager.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2022 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 reactor.core.publisher.Mono; + +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.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; + +/** + * A {@link ReactiveAuthorizationManager} which can determine if an {@link Authentication} + * has access to the {@link MethodInvocation} by evaluating an expression from the + * {@link PreAuthorize} annotation. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class PreAuthorizeReactiveAuthorizationManager implements ReactiveAuthorizationManager { + + private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry(); + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + this.registry.setExpressionHandler(expressionHandler); + } + + /** + * Determines if an {@link Authentication} has access to the {@link MethodInvocation} + * by evaluating an expression from the {@link PreAuthorize} annotation. + * @param authentication the {@link Mono} of the {@link Authentication} to check + * @param mi the {@link MethodInvocation} to check + * @return a {@link Mono} of the {@link AuthorizationDecision} or an empty + * {@link Mono} if the {@link PreAuthorize} annotation is not present + */ + @Override + public Mono check(Mono authentication, MethodInvocation mi) { + ExpressionAttribute attribute = this.registry.getAttribute(mi); + if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) { + return Mono.empty(); + } + // @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)); + // @formatter:on + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java index 758596f2cf6..06d2472cd22 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java @@ -16,7 +16,6 @@ package org.springframework.security.authorization.method; -import java.lang.reflect.Method; import java.util.function.Supplier; import org.aopalliance.aop.Advice; @@ -26,12 +25,8 @@ import org.springframework.aop.Pointcut; import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.framework.AopInfrastructureBean; -import org.springframework.aop.support.AopUtils; import org.springframework.core.Ordered; import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.lang.NonNull; -import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; @@ -61,8 +56,6 @@ public final class PreFilterAuthorizationMethodInterceptor private final Pointcut pointcut; - private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - /** * Creates a {@link PreFilterAuthorizationMethodInterceptor} using the provided * parameters @@ -76,8 +69,7 @@ public PreFilterAuthorizationMethodInterceptor() { * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use */ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { - Assert.notNull(expressionHandler, "expressionHandler cannot be null"); - this.expressionHandler = expressionHandler; + this.registry.setExpressionHandler(expressionHandler); } /** @@ -127,13 +119,14 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy strat */ @Override public Object invoke(MethodInvocation mi) throws Throwable { - PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi); - if (attribute == PreFilterExpressionAttribute.NULL_ATTRIBUTE) { + PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi); + if (attribute == PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute.NULL_ATTRIBUTE) { return mi.proceed(); } - EvaluationContext ctx = this.expressionHandler.createEvaluationContext(this.authentication, mi); - Object filterTarget = findFilterTarget(attribute.filterTarget, ctx, mi); - this.expressionHandler.filter(filterTarget, attribute.getExpression(), ctx); + MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler(); + EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi); + Object filterTarget = findFilterTarget(attribute.getFilterTarget(), ctx, mi); + expressionHandler.filter(filterTarget, attribute.getExpression(), ctx); return mi.proceed(); } @@ -168,41 +161,4 @@ private Supplier getAuthentication(SecurityContextHolderStrategy }; } - private final class PreFilterExpressionAttributeRegistry - extends AbstractExpressionAttributeRegistry { - - @NonNull - @Override - PreFilterExpressionAttribute resolveAttribute(Method method, Class targetClass) { - Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); - PreFilter preFilter = findPreFilterAnnotation(specificMethod); - if (preFilter == null) { - return PreFilterExpressionAttribute.NULL_ATTRIBUTE; - } - Expression preFilterExpression = PreFilterAuthorizationMethodInterceptor.this.expressionHandler - .getExpressionParser().parseExpression(preFilter.value()); - return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget()); - } - - private PreFilter findPreFilterAnnotation(Method method) { - PreFilter preFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreFilter.class); - return (preFilter != null) ? preFilter - : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreFilter.class); - } - - } - - private static final class PreFilterExpressionAttribute extends ExpressionAttribute { - - private static final PreFilterExpressionAttribute NULL_ATTRIBUTE = new PreFilterExpressionAttribute(null, null); - - private final String filterTarget; - - private PreFilterExpressionAttribute(Expression expression, String filterTarget) { - super(expression); - this.filterTarget = filterTarget; - } - - } - } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java new file mode 100644 index 00000000000..3746e71fd60 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptor.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2022 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 java.lang.reflect.Method; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.access.prepost.PreFilter; +import org.springframework.security.core.parameters.DefaultSecurityParameterNameDiscoverer; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link MethodInterceptor} which filters a reactive method argument by evaluating an + * expression from the {@link PreFilter} annotation. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +public final class PreFilterAuthorizationReactiveMethodInterceptor implements Ordered, MethodInterceptor, + PointcutAdvisor, AopInfrastructureBean, BeanDefinitionRegistryPostProcessor { + + private final AuthorizationBeanFactoryPostProcessor beanFactoryPostProcessor = new AuthorizationBeanFactoryPostProcessor(); + + private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry(); + + private final Pointcut pointcut = AuthorizationMethodPointcuts.forAnnotations(PreFilter.class); + + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer(); + + private int order = AuthorizationInterceptorsOrder.PRE_FILTER.getOrder(); + + /** + * Sets the {@link MethodSecurityExpressionHandler}. + * @param expressionHandler the {@link MethodSecurityExpressionHandler} to use + */ + public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + this.registry.setExpressionHandler(expressionHandler); + } + + /** + * Sets the {@link ParameterNameDiscoverer}. + * @param parameterNameDiscoverer the {@link ParameterNameDiscoverer} to use + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + Assert.notNull(parameterNameDiscoverer, "parameterNameDiscoverer cannot be null"); + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * Filters a reactive method argument by evaluating an expression from the + * {@link PreFilter} annotation. + * @param mi the {@link MethodInvocation} to use + * @return the {@link Publisher} to use + */ + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi); + if (attribute == PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute.NULL_ATTRIBUTE) { + return ReactiveMethodInvocationUtils.>proceed(mi); + } + FilterTarget filterTarget = findFilterTarget(attribute.getFilterTarget(), mi); + Mono toInvoke = ReactiveAuthenticationUtils.getAuthentication() + .map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi)); + if (filterTarget.value instanceof Mono) { + mi.getArguments()[filterTarget.index] = toInvoke + .flatMap((ctx) -> filterMono((Mono) filterTarget.value, attribute.getExpression(), ctx)); + } + else { + Flux result = toInvoke + .flatMapMany((ctx) -> filterPublisher(filterTarget.value, attribute.getExpression(), ctx)); + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance() + .getAdapter(filterTarget.value.getClass()); + mi.getArguments()[filterTarget.index] = (adapter != null) ? adapter.fromPublisher(result) : result; + } + return ReactiveMethodInvocationUtils.>proceed(mi); + } + + private FilterTarget findFilterTarget(String name, MethodInvocation mi) { + Object value = null; + int index = 0; + if (StringUtils.hasText(name)) { + Object target = mi.getThis(); + Class targetClass = (target != null) ? AopUtils.getTargetClass(target) : null; + Method specificMethod = AopUtils.getMostSpecificMethod(mi.getMethod(), targetClass); + String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(specificMethod); + if (parameterNames != null && parameterNames.length > 0) { + Object[] arguments = mi.getArguments(); + for (index = 0; index < parameterNames.length; index++) { + if (name.equals(parameterNames[index])) { + value = arguments[index]; + break; + } + } + Assert.notNull(value, + "Filter target was null, or no argument with name '" + name + "' found in method."); + } + } + else { + Object[] arguments = mi.getArguments(); + Assert.state(arguments.length == 1, + "Unable to determine the method argument for filtering. Specify the filter target."); + value = arguments[0]; + Assert.notNull(value, + "Filter target was null. Make sure you passing the correct value in the method argument."); + } + Assert.state(value instanceof Publisher, "Filter target must be an instance of Publisher."); + return new FilterTarget((Publisher) value, index); + } + + private Mono filterMono(Mono filterTarget, Expression filterExpression, EvaluationContext ctx) { + MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject() + .getValue(); + return filterTarget.filterWhen((filterObject) -> { + rootObject.setFilterObject(filterObject); + return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx); + }); + } + + private Flux filterPublisher(Publisher filterTarget, Expression filterExpression, EvaluationContext ctx) { + MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject() + .getValue(); + return Flux.from(filterTarget).filterWhen((filterObject) -> { + rootObject.setFilterObject(filterObject); + return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx); + }); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public Advice getAdvice() { + return this; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + this.beanFactoryPostProcessor.postProcessBeanDefinitionRegistry(registry); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.beanFactoryPostProcessor.postProcessBeanFactory(beanFactory); + } + + private static final class FilterTarget { + + private final Publisher value; + + private final int index; + + private FilterTarget(Publisher value, int index) { + this.value = value; + this.index = index; + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java new file mode 100644 index 00000000000..7d5e0befb47 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2022 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 java.lang.reflect.Method; + +import org.springframework.aop.support.AopUtils; +import org.springframework.expression.Expression; +import org.springframework.lang.NonNull; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.prepost.PreFilter; +import org.springframework.util.Assert; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class PreFilterExpressionAttributeRegistry + extends AbstractExpressionAttributeRegistry { + + private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + + MethodSecurityExpressionHandler getExpressionHandler() { + return this.expressionHandler; + } + + void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { + Assert.notNull(expressionHandler, "expressionHandler cannot be null"); + this.expressionHandler = expressionHandler; + } + + @NonNull + @Override + PreFilterExpressionAttribute resolveAttribute(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + PreFilter preFilter = findPreFilterAnnotation(specificMethod); + if (preFilter == null) { + return PreFilterExpressionAttribute.NULL_ATTRIBUTE; + } + Expression preFilterExpression = this.expressionHandler.getExpressionParser() + .parseExpression(preFilter.value()); + return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget()); + } + + private PreFilter findPreFilterAnnotation(Method method) { + PreFilter preFilter = AuthorizationAnnotationUtils.findUniqueAnnotation(method, PreFilter.class); + return (preFilter != null) ? preFilter + : AuthorizationAnnotationUtils.findUniqueAnnotation(method.getDeclaringClass(), PreFilter.class); + } + + static final class PreFilterExpressionAttribute extends ExpressionAttribute { + + static final PreFilterExpressionAttribute NULL_ATTRIBUTE = new PreFilterExpressionAttribute(null, null); + + private final String filterTarget; + + private PreFilterExpressionAttribute(Expression expression, String filterTarget) { + super(expression); + this.filterTarget = filterTarget; + } + + String getFilterTarget() { + return this.filterTarget; + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java new file mode 100644 index 00000000000..433587cf098 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveAuthenticationUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2022 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 reactor.core.publisher.Mono; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class ReactiveAuthenticationUtils { + + private static final Authentication ANONYMOUS = new AnonymousAuthenticationToken("key", "anonymous", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + static Mono getAuthentication() { + return ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication) + .defaultIfEmpty(ANONYMOUS); + } + + private ReactiveAuthenticationUtils() { + } + +} 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 new file mode 100644 index 00000000000..2675bb96dc7 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveExpressionUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2022 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 reactor.core.publisher.Mono; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class ReactiveExpressionUtils { + + static Mono evaluateAsBoolean(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 Boolean) { + return Mono.just((Boolean) value); + } + if (value instanceof Mono) { + Mono monoValue = (Mono) value; + // @formatter:off + return monoValue + .filter(Boolean.class::isInstance) + .map(Boolean.class::cast) + .switchIfEmpty(createInvalidReturnTypeMono(expr)); + // @formatter:on + } + return createInvalidReturnTypeMono(expr); + }); + } + + private static Mono createInvalidReturnTypeMono(Expression expr) { + return Mono.error(() -> new IllegalStateException( + "Expression: '" + expr.getExpressionString() + "' must return boolean or Mono")); + } + + private ReactiveExpressionUtils() { + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java b/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java new file mode 100644 index 00000000000..7b484525abb --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/method/ReactiveMethodInvocationUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2022 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 reactor.core.Exceptions; + +/** + * For internal use only, as this contract is likely to change. + * + * @author Evgeniy Cheban + * @since 5.8 + */ +final class ReactiveMethodInvocationUtils { + + static T proceed(MethodInvocation mi) { + try { + return (T) mi.proceed(); + } + catch (Throwable ex) { + throw Exceptions.propagate(ex); + } + } + + private ReactiveMethodInvocationUtils() { + } + +} 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 new file mode 100644 index 00000000000..5b03baa7552 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerAfterReactiveMethodInterceptorTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2022 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.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.Pointcut; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authorization.ReactiveAuthorizationManager; + +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; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AuthorizationManagerAfterReactiveMethodInterceptor}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationManagerAfterReactiveMethodInterceptorTests { + + @Test + public void instantiateWhenPointcutNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerAfterReactiveMethodInterceptor(null, + mock(ReactiveAuthorizationManager.class))) + .withMessage("pointcut cannot be null"); + } + + @Test + public void instantiateWhenAuthorizationManagerNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerAfterReactiveMethodInterceptor(mock(Pointcut.class), null)) + .withMessage("authorizationManager cannot be null"); + } + + @Test + public void invokeMonoWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable { + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty()); + 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()); + } + + @Test + public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable { + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + given(mockReactiveAuthorizationManager.verify(any(), any())).willReturn(Mono.empty()); + 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"); + verify(mockReactiveAuthorizationManager, times(2)).verify(any(), any()); + } + + @Test + public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedException() throws Throwable { + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + given(mockReactiveAuthorizationManager.verify(any(), any())) + .willReturn(Mono.error(new AccessDeniedException("Access Denied"))); + AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> assertThat(result) + .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block)) + .withMessage("Access Denied"); + verify(mockReactiveAuthorizationManager).verify(any(), any()); + } + +} 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 new file mode 100644 index 00000000000..2127953685d --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeReactiveMethodInterceptorTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2022 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.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.Pointcut; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authorization.ReactiveAuthorizationManager; + +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.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AuthorizationManagerBeforeReactiveMethodInterceptor}. + * + * @author Evgeniy Cheban + */ +public class AuthorizationManagerBeforeReactiveMethodInterceptorTests { + + @Test + public void instantiateWhenPointcutNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerBeforeReactiveMethodInterceptor(null, + mock(ReactiveAuthorizationManager.class))) + .withMessage("pointcut cannot be null"); + + } + + @Test + public void instantiateWhenAuthorizationManagerNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerBeforeReactiveMethodInterceptor(mock(Pointcut.class), null)) + .withMessage("authorizationManager cannot be null"); + } + + @Test + public void invokeMonoWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable { + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + given(mockMethodInvocation.proceed()).willReturn(Mono.just("john")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); + 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)); + } + + @Test + public void invokeFluxWhenMockReactiveAuthorizationManagerThenVerify() throws Throwable { + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob")); + ReactiveAuthorizationManager mockReactiveAuthorizationManager = mock( + ReactiveAuthorizationManager.class); + given(mockReactiveAuthorizationManager.verify(any(), eq(mockMethodInvocation))).willReturn(Mono.empty()); + 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("john", "bob"); + verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation)); + } + + @Test + public void invokeWhenMockReactiveAuthorizationManagerDeniedThenAccessDeniedException() throws Throwable { + MethodInvocation mockMethodInvocation = mock(MethodInvocation.class); + 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"))); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor( + Pointcut.TRUE, mockReactiveAuthorizationManager); + Object result = interceptor.invoke(mockMethodInvocation); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> assertThat(result) + .asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block)) + .withMessage("Access Denied"); + verify(mockReactiveAuthorizationManager).verify(any(), eq(mockMethodInvocation)); + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java index b8e759c93a9..4954b009062 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -51,7 +51,7 @@ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager(); manager.setExpressionHandler(expressionHandler); - assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler); + assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); } @Test diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManagerTests.java new file mode 100644 index 00000000000..d7d8083ccc2 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PostAuthorizeReactiveAuthorizationManagerTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2002-2022 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.annotation.AnnotationConfigurationException; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PostAuthorizeReactiveAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class PostAuthorizeReactiveAuthorizationManagerTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + manager.setExpressionHandler(expressionHandler); + assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething", new Class[] {}, new Object[] {}); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null); + AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block(); + assertThat(decision).isNull(); + } + + @Test + public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "grant" }); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null); + AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "deny" }); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void checkDoSomethingListWhenReturnObjectContainsGrantThenGrantedDecision() throws Exception { + List list = Arrays.asList("grant", "deny"); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingList", new Class[] { List.class }, new Object[] { list }); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, list); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkDoSomethingListWhenReturnObjectNotContainsGrantThenDeniedDecision() throws Exception { + List list = Collections.singletonList("deny"); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingList", new Class[] { List.class }, new Object[] { list }); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, list); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager.check(ReactiveAuthenticationUtils.getAuthentication(), result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(), + ClassLevelAnnotations.class, "securedAdmin"); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager.check(authentication, result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN")); + decision = manager.check(authentication, result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(), + ClassLevelAnnotations.class, "securedUser"); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager.check(authentication, result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN")); + decision = manager.check(authentication, result).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "inheritedAnnotations"); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> manager.check(authentication, result)); + } + + @Test + public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(), + ClassLevelAnnotations.class, "inheritedAnnotations"); + MethodInvocationResult result = new MethodInvocationResult(methodInvocation, null); + PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> manager.check(authentication, result)); + } + + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { + + public void doSomething() { + + } + + @PostAuthorize("#s == 'grant'") + public String doSomethingString(String s) { + return s; + } + + @PostAuthorize("returnObject.contains('grant')") + public List doSomethingList(List list) { + return list; + } + + @Override + public void inheritedAnnotations() { + + } + + } + + @PostAuthorize("hasRole('USER')") + public static class ClassLevelAnnotations implements InterfaceAnnotationsThree { + + @PostAuthorize("hasRole('ADMIN')") + public void securedAdmin() { + + } + + public void securedUser() { + + } + + @Override + @PostAuthorize("hasRole('ADMIN')") + public void inheritedAnnotations() { + + } + + } + + public interface InterfaceAnnotationsOne { + + @PostAuthorize("hasRole('ADMIN')") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsTwo { + + @PostAuthorize("hasRole('USER')") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsThree { + + @MyPostAuthorize + void inheritedAnnotations(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @PostAuthorize("hasRole('USER')") + public @interface MyPostAuthorize { + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java index 683c80eddb4..4153144ff7d 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptorTests.java @@ -67,7 +67,7 @@ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); PostFilterAuthorizationMethodInterceptor advice = new PostFilterAuthorizationMethodInterceptor(); advice.setExpressionHandler(expressionHandler); - assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler); + assertThat(advice).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); } @Test diff --git a/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptorTests.java new file mode 100644 index 00000000000..000c080c706 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PostFilterAuthorizationReactiveMethodInterceptorTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2022 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.annotation.AnnotationConfigurationException; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.prepost.PostFilter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PostFilterAuthorizationReactiveMethodInterceptor}. + * + * @author Evgeniy Cheban + */ +public class PostFilterAuthorizationReactiveMethodInterceptorTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + interceptor.setExpressionHandler(expressionHandler); + assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void methodMatcherWhenMethodHasNotPostFilterAnnotationThenNotMatches() throws Exception { + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + assertThat(interceptor.getPointcut().getMethodMatcher() + .matches(NoPostFilterClass.class.getMethod("doSomething"), NoPostFilterClass.class)).isFalse(); + } + + @Test + public void methodMatcherWhenMethodHasPostFilterAnnotationThenMatches() throws Exception { + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + assertThat(interceptor.getPointcut().getMethodMatcher() + .matches(TestClass.class.getMethod("doSomethingFlux", Flux.class), TestClass.class)).isTrue(); + } + + @Test + public void invokeWhenMonoThenFilteredMono() throws Throwable { + Mono mono = Mono.just("bob"); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingMono", new Class[] { Mono.class }, new Object[] { mono }) { + @Override + public Object proceed() { + return mono; + } + }; + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + Object result = interceptor.invoke(methodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block).isNull(); + } + + @Test + public void invokeWhenFluxThenFilteredFlux() throws Throwable { + Flux flux = Flux.just("john", "bob"); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingFluxClassLevel", new Class[] { Flux.class }, new Object[] { flux }) { + @Override + public Object proceed() { + return flux; + } + }; + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + Object result = interceptor.invoke(methodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)).extracting(Flux::collectList) + .extracting(Mono::block, InstanceOfAssertFactories.list(String.class)).containsOnly("john"); + } + + @Test + public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "inheritedAnnotations"); + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> interceptor.invoke(methodInvocation)); + } + + @Test + public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ConflictingAnnotations(), + ConflictingAnnotations.class, "inheritedAnnotations"); + PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> interceptor.invoke(methodInvocation)); + } + + @PostFilter("filterObject == 'john'") + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { + + @PostFilter("filterObject == 'john'") + public Flux doSomethingFlux(Flux flux) { + return flux; + } + + public Flux doSomethingFluxClassLevel(Flux flux) { + return flux; + } + + @PostFilter("filterObject == 'john'") + public Mono doSomethingMono(Mono mono) { + return mono; + } + + @Override + public void inheritedAnnotations() { + + } + + } + + public static class NoPostFilterClass { + + public void doSomething() { + + } + + } + + public static class ConflictingAnnotations implements InterfaceAnnotationsThree { + + @Override + @PostFilter("filterObject == 'jack'") + public void inheritedAnnotations() { + + } + + } + + public interface InterfaceAnnotationsOne { + + @PostFilter("filterObject == 'jim'") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsTwo { + + @PostFilter("filterObject == 'jane'") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsThree { + + @MyPostFilter + void inheritedAnnotations(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @PostFilter("filterObject == 'john'") + public @interface MyPostFilter { + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java index 83cbe5cdb93..d85e85ac9d4 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -49,7 +49,7 @@ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager(); manager.setExpressionHandler(expressionHandler); - assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler); + assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); } @Test diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java new file mode 100644 index 00000000000..f4ba70921e2 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PreAuthorizeReactiveAuthorizationManagerTests.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2022 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.annotation.AnnotationConfigurationException; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.core.Authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PreAuthorizeReactiveAuthorizationManager}. + * + * @author Evgeniy Cheban + */ +public class PreAuthorizeReactiveAuthorizationManagerTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + manager.setExpressionHandler(expressionHandler); + assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void checkDoSomethingWhenNoPostAuthorizeAnnotationThenNullDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomething", new Class[] {}, new Object[] {}); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager + .check(ReactiveAuthenticationUtils.getAuthentication(), methodInvocation).block(); + assertThat(decision).isNull(); + } + + @Test + public void checkDoSomethingStringWhenArgIsGrantThenGrantedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "grant" }); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager + .check(ReactiveAuthenticationUtils.getAuthentication(), methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkDoSomethingStringWhenArgIsNotGrantThenDeniedDecision() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingString", new Class[] { String.class }, new Object[] { "deny" }); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager + .check(ReactiveAuthenticationUtils.getAuthentication(), methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void checkRequiresAdminWhenClassAnnotationsThenMethodAnnotationsTakePrecedence() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(), + ClassLevelAnnotations.class, "securedAdmin"); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager.check(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN")); + decision = manager.check(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void checkRequiresUserWhenClassAnnotationsThenApplies() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(), + ClassLevelAnnotations.class, "securedUser"); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + AuthorizationDecision decision = manager.check(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + authentication = Mono.just(new TestingAuthenticationToken("user", "password", "ROLE_ADMIN")); + decision = manager.check(authentication, methodInvocation).block(); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "inheritedAnnotations"); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> manager.check(authentication, methodInvocation)); + } + + @Test + public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception { + Mono authentication = Mono + .just(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ClassLevelAnnotations(), + ClassLevelAnnotations.class, "inheritedAnnotations"); + PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> manager.check(authentication, methodInvocation)); + } + + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { + + public void doSomething() { + + } + + @PreAuthorize("#s == 'grant'") + public String doSomethingString(String s) { + return s; + } + + @Override + public void inheritedAnnotations() { + + } + + } + + @PreAuthorize("hasRole('USER')") + public static class ClassLevelAnnotations implements InterfaceAnnotationsThree { + + @PreAuthorize("hasRole('ADMIN')") + public void securedAdmin() { + + } + + public void securedUser() { + + } + + @Override + @PreAuthorize("hasRole('ADMIN')") + public void inheritedAnnotations() { + + } + + } + + public interface InterfaceAnnotationsOne { + + @PreAuthorize("hasRole('ADMIN')") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsTwo { + + @PreAuthorize("hasRole('USER')") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsThree { + + @MyPreAuthorize + void inheritedAnnotations(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @PreAuthorize("hasRole('USER')") + public @interface MyPreAuthorize { + + } + +} diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java index 036d427711a..f215a99a72a 100644 --- a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java +++ b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptorTests.java @@ -69,7 +69,7 @@ public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); PreFilterAuthorizationMethodInterceptor advice = new PreFilterAuthorizationMethodInterceptor(); advice.setExpressionHandler(expressionHandler); - assertThat(advice).extracting("expressionHandler").isEqualTo(expressionHandler); + assertThat(advice).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); } @Test diff --git a/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptorTests.java b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptorTests.java new file mode 100644 index 00000000000..65430c4f61f --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/method/PreFilterAuthorizationReactiveMethodInterceptorTests.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2022 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotationConfigurationException; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.intercept.method.MockMethodInvocation; +import org.springframework.security.access.prepost.PreFilter; +import org.springframework.security.core.parameters.DefaultSecurityParameterNameDiscoverer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PreFilterAuthorizationReactiveMethodInterceptor}. + * + * @author Evgeniy Cheban + */ +public class PreFilterAuthorizationReactiveMethodInterceptorTests { + + @Test + public void setExpressionHandlerWhenNotNullThenSetsExpressionHandler() { + MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + interceptor.setExpressionHandler(expressionHandler); + assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler); + } + + @Test + public void setExpressionHandlerWhenNullThenException() { + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExpressionHandler(null)) + .withMessage("expressionHandler cannot be null"); + } + + @Test + public void setParameterNameDiscovererWhenNotNullThenSetsParameterNameDiscoverer() { + ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer(); + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + interceptor.setParameterNameDiscoverer(parameterNameDiscoverer); + assertThat(interceptor).extracting("parameterNameDiscoverer").isEqualTo(parameterNameDiscoverer); + } + + @Test + public void setParameterNameDiscovererWhenNullThenException() { + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setParameterNameDiscoverer(null)) + .withMessage("parameterNameDiscoverer cannot be null"); + } + + @Test + public void methodMatcherWhenMethodHasNotPreFilterAnnotationThenNotMatches() throws Exception { + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThat(interceptor.getPointcut().getMethodMatcher().matches(NoPreFilterClass.class.getMethod("doSomething"), + NoPreFilterClass.class)).isFalse(); + } + + @Test + public void methodMatcherWhenMethodHasPreFilterAnnotationThenMatches() throws Exception { + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThat(interceptor.getPointcut().getMethodMatcher() + .matches(TestClass.class.getMethod("doSomethingFluxFilterTargetMatch", Flux.class), TestClass.class)) + .isTrue(); + } + + @Test + public void findFilterTargetWhenNameProvidedAndNotMatchThenException() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingFluxFilterTargetNotMatch", new Class[] { Flux.class }, new Object[] { Flux.empty() }); + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.invoke(methodInvocation)).withMessage( + "Filter target was null, or no argument with name 'filterTargetNotMatch' found in method."); + } + + @Test + public void findFilterTargetWhenNameProvidedAndMatchAndNullThenException() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingFluxFilterTargetMatch", new Class[] { Flux.class }, new Object[] { null }); + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThatIllegalArgumentException().isThrownBy(() -> interceptor.invoke(methodInvocation)) + .withMessage("Filter target was null, or no argument with name 'flux' found in method."); + } + + @Test + public void findFilterTargetWhenNameNotProvidedAndSingleArgMonoThenFiltersMono() throws Throwable { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingMonoFilterTargetNotProvided", new Class[] { Mono.class }, + new Object[] { Mono.just("bob") }) { + @Override + public Object proceed() { + return getArguments()[0]; + } + }; + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + Object result = interceptor.invoke(methodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Mono.class)).extracting(Mono::block).isNull(); + } + + @Test + public void findFilterTargetWhenNameNotProvidedAndSingleArgFluxThenFiltersFlux() throws Throwable { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "doSomethingFluxFilterTargetNotProvided", new Class[] { Flux.class }, + new Object[] { Flux.just("john", "bob") }) { + @Override + public Object proceed() { + return getArguments()[0]; + } + }; + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + Object result = interceptor.invoke(methodInvocation); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.type(Flux.class)).extracting(Flux::collectList) + .extracting(Mono::block, InstanceOfAssertFactories.list(String.class)).containsOnly("john"); + } + + @Test + public void checkInheritedAnnotationsWhenDuplicatedThenAnnotationConfigurationException() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new TestClass(), TestClass.class, + "inheritedAnnotations"); + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> interceptor.invoke(methodInvocation)); + } + + @Test + public void checkInheritedAnnotationsWhenConflictingThenAnnotationConfigurationException() throws Exception { + MockMethodInvocation methodInvocation = new MockMethodInvocation(new ConflictingAnnotations(), + ConflictingAnnotations.class, "inheritedAnnotations"); + PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> interceptor.invoke(methodInvocation)); + } + + @PreFilter("filterObject == 'john'") + public static class TestClass implements InterfaceAnnotationsOne, InterfaceAnnotationsTwo { + + @PreFilter(value = "filterObject == 'john'", filterTarget = "filterTargetNotMatch") + public Flux doSomethingFluxFilterTargetNotMatch(Flux flux) { + return flux; + } + + @PreFilter(value = "filterObject == 'john'", filterTarget = "flux") + public Flux doSomethingFluxFilterTargetMatch(Flux flux) { + return flux; + } + + @PreFilter("filterObject == 'john'") + public Flux doSomethingFluxFilterTargetNotProvided(Flux flux) { + return flux; + } + + @PreFilter("filterObject == 'john'") + public Mono doSomethingMonoFilterTargetNotProvided(Mono mono) { + return mono; + } + + public Flux doSomethingTwoArgsFilterTargetNotProvided(String s, Flux flux) { + return flux; + } + + @Override + public void inheritedAnnotations() { + + } + + } + + public static class NoPreFilterClass { + + public void doSomething() { + + } + + } + + public static class ConflictingAnnotations implements InterfaceAnnotationsThree { + + @Override + @PreFilter("filterObject == 'jack'") + public void inheritedAnnotations() { + + } + + } + + public interface InterfaceAnnotationsOne { + + @PreFilter("filterObject == 'jim'") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsTwo { + + @PreFilter("filterObject == 'jane'") + void inheritedAnnotations(); + + } + + public interface InterfaceAnnotationsThree { + + @MyPreFilter + void inheritedAnnotations(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @PreFilter("filterObject == 'john'") + public @interface MyPreFilter { + + } + +}