Merge remote-tracking branch 'origin/5.8.x' into main

This commit is contained in:
Josh Cummings 2022-08-25 14:46:48 -06:00
commit 84f765a89c
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
38 changed files with 3394 additions and 191 deletions

View File

@ -25,6 +25,7 @@ import java.lang.annotation.Target;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
/**
*
@ -67,4 +68,11 @@ public @interface EnableReactiveMethodSecurity {
*/
int order() default Ordered.LOWEST_PRECEDENCE;
/**
* Indicate whether {@link ReactiveAuthorizationManager} based Method Security to be
* used.
* @since 5.8
*/
boolean useAuthorizationManager() default false;
}

View File

@ -0,0 +1,87 @@
/*
* 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) {
return new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler) {
PreAuthorizeReactiveAuthorizationManager authorizationManager = new PreAuthorizeReactiveAuthorizationManager(
expressionHandler);
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
MethodSecurityExpressionHandler expressionHandler) {
return new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler) {
PostAuthorizeReactiveAuthorizationManager authorizationManager = new PostAuthorizeReactiveAuthorizationManager(
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;
}
}

View File

@ -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<EnableReactiveMethodSecurity> {
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];
}
throw new IllegalStateException("AdviceMode " + adviceMode + " is not supported");
EnableReactiveMethodSecurity annotation = importMetadata.getAnnotations()
.get(EnableReactiveMethodSecurity.class).synthesize();
List<String> imports = new ArrayList<>(Arrays.asList(this.autoProxy.selectImports(importMetadata)));
if (annotation.useAuthorizationManager()) {
imports.add(ReactiveAuthorizationManagerMethodSecurityConfiguration.class.getName());
}
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}.
* <p>
* Take care of adding the necessary JSR-107 import if it is available.
*/
private String[] getProxyImports() {
List<String> 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<EnableReactiveMethodSecurity> {
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");
}
}
}

View File

@ -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 class Authz {
return id % 2 == 0;
}
public Mono<Boolean> 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());
}

View File

@ -21,7 +21,9 @@ import reactor.core.publisher.Flux;
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 class DelegatingReactiveMessageService implements ReactiveMessageService
return this.delegate.monoPreAuthorizeBeanFindById(id);
}
@Override
@PreAuthorize("@authz.checkReactive(#id)")
public Mono<String> monoPreAuthorizeBeanFindByIdReactiveExpression(long id) {
return this.delegate.monoPreAuthorizeBeanFindByIdReactiveExpression(id);
}
@Override
@PostAuthorize("@authz.check(authentication, returnObject)")
public Mono<String> monoPostAuthorizeBeanFindById(long id) {
@ -95,6 +103,20 @@ public class DelegatingReactiveMessageService implements ReactiveMessageService
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<String> fluxManyAnnotations(Flux<String> flux) {
return flux;
}
@PostFilter("filterObject.length > 5")
public Flux<String> fluxPostFilter(Flux<String> flux) {
return flux;
}
@Override
public Publisher<String> publisherFindById(long id) {
return this.delegate.publisherFindById(id);

View File

@ -0,0 +1,484 @@
/*
* 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#useAuthorizationManager()} flag set to true.
*
* @author Evgeniy Cheban
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class EnableAuthorizationManagerReactiveMethodSecurityTests {
@Autowired
ReactiveMessageService messageService;
ReactiveMessageService delegate;
TestPublisher<String> 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 (for example, a Mono or Flux) 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> findById = this.messageService.fluxPostAuthorizeBeanFindById(1L).contextWrite(this.withUser);
StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
}
@Test
public void fluxManyAnnotationsWhenMeetsConditionsThenReturnsFilteredFlux() {
Flux<String> 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<String> 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<String> flux = this.messageService
.fluxManyAnnotations(Flux.just("harold", "jonathan", "michael", "pete", "bo"))
.contextWrite(this.withAdmin);
StepVerifier.create(flux).expectNext("harold", "jonathan").expectError(AccessDeniedException.class).verify();
}
@Test
public void fluxPostFilterWhenFilteringThenWorks() {
Flux<String> flux = this.messageService.fluxPostFilter(Flux.just("harold", "jonathan", "michael", "pete", "bo"))
.contextWrite(this.withAdmin);
StepVerifier.create(flux).expectNext("harold", "jonathan", "michael").verifyComplete();
}
// 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<String> 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<String> 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<String> 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<String> 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<String> findById = this.messageService.publisherPreAuthorizeBeanFindById(2L);
StepVerifier.create(findById).expectNext("result").verifyComplete();
}
@Test
public void publisherPreAuthorizeBeanWhenNoAuthenticationThenDenied() {
given(this.delegate.publisherPreAuthorizeBeanFindById(1L)).willReturn(this.result);
Publisher<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> findById = Flux.from(this.messageService.publisherPostAuthorizeBeanFindById(1L))
.contextWrite(this.withUser);
StepVerifier.create(findById).expectError(AccessDeniedException.class).verify();
}
static <T> Publisher<T> publisher(Flux<T> flux) {
return (subscriber) -> flux.subscribe(subscriber);
}
static <T> Publisher<T> publisherJust(T... data) {
return publisher(Flux.just(data));
}
@EnableReactiveMethodSecurity(useAuthorizationManager = true)
static class Config {
ReactiveMessageService delegate = mock(ReactiveMessageService.class);
@Bean
DelegatingReactiveMessageService defaultMessageService() {
return new DelegatingReactiveMessageService(this.delegate);
}
@Bean
Authz authz() {
return new Authz();
}
}
}

View File

@ -32,6 +32,8 @@ public interface ReactiveMessageService {
Mono<String> monoPreAuthorizeBeanFindById(long id);
Mono<String> monoPreAuthorizeBeanFindByIdReactiveExpression(long id);
Mono<String> monoPostAuthorizeBeanFindById(long id);
Flux<String> fluxFindById(long id);
@ -44,6 +46,10 @@ public interface ReactiveMessageService {
Flux<String> fluxPostAuthorizeBeanFindById(long id);
Flux<String> fluxManyAnnotations(Flux<String> flux);
Flux<String> fluxPostFilter(Flux<String> flux);
Publisher<String> publisherFindById(long id);
Publisher<String> publisherPreAuthorizeHasRoleFindById(long id);

View File

@ -52,7 +52,12 @@ import org.springframework.util.Assert;
* @author Rob Winch
* @author Eleftheria Stein
* @since 5.0
* @deprecated Use
* {@link org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor}
* or
* {@link org.springframework.security.authorization.method.AuthorizationManagerAfterReactiveMethodInterceptor}
*/
@Deprecated
public class PrePostAdviceReactiveMethodInterceptor implements MethodInterceptor {
private Authentication anonymous = new AnonymousAuthenticationToken("key", "anonymous",

View File

@ -0,0 +1,153 @@
/*
* 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 java.util.function.Function;
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.core.Ordered;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
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 {
private final Pointcut pointcut;
private final ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager;
private int order = AuthorizationInterceptorsOrder.LAST.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<MethodInvocationResult> authorizationManager) {
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(
AuthorizationMethodPointcuts.forAnnotations(PostAuthorize.class), authorizationManager);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder());
return interceptor;
}
/**
* Creates an instance.
* @param pointcut the {@link Pointcut} to use
* @param authorizationManager the {@link ReactiveAuthorizationManager} to use
*/
public AuthorizationManagerAfterReactiveMethodInterceptor(Pointcut pointcut,
ReactiveAuthorizationManager<MethodInvocationResult> 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 {
Method method = mi.getMethod();
Class<?> type = method.getReturnType();
Assert.state(Publisher.class.isAssignableFrom(type),
() -> String.format("The returnType %s on %s must return an instance of org.reactivestreams.Publisher "
+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
Function<Object, Mono<?>> postAuthorize = (result) -> postAuthorize(authentication, mi, result);
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
Publisher<?> publisher = ReactiveMethodInvocationUtils.proceed(mi);
if (isMultiValue(type, adapter)) {
Flux<?> flux = Flux.from(publisher).flatMap(postAuthorize);
return (adapter != null) ? adapter.fromPublisher(flux) : flux;
}
Mono<?> mono = Mono.from(publisher).flatMap(postAuthorize);
return (adapter != null) ? adapter.fromPublisher(mono) : mono;
}
private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
if (Flux.class.isAssignableFrom(returnType)) {
return true;
}
return adapter == null || adapter.isMultiValue();
}
private Mono<?> postAuthorize(Mono<Authentication> 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;
}
}

View File

@ -0,0 +1,149 @@
/*
* 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.core.Ordered;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
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
* @author Josh Cummings
* @since 5.8
*/
public final class AuthorizationManagerBeforeReactiveMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
private final Pointcut pointcut;
private final ReactiveAuthorizationManager<MethodInvocation> authorizationManager;
private int order = AuthorizationInterceptorsOrder.FIRST.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<MethodInvocation> authorizationManager) {
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(
AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
return interceptor;
}
/**
* Creates an instance.
* @param pointcut the {@link Pointcut} to use
* @param authorizationManager the {@link ReactiveAuthorizationManager} to use
*/
public AuthorizationManagerBeforeReactiveMethodInterceptor(Pointcut pointcut,
ReactiveAuthorizationManager<MethodInvocation> 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 {
Method method = mi.getMethod();
Class<?> type = method.getReturnType();
Assert.state(Publisher.class.isAssignableFrom(type),
() -> String.format("The returnType %s on %s must return an instance of org.reactivestreams.Publisher "
+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
Mono<Authentication> authentication = ReactiveAuthenticationUtils.getAuthentication();
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
Mono<Void> preAuthorize = this.authorizationManager.verify(authentication, mi);
if (isMultiValue(type, adapter)) {
Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Flux<?> result = preAuthorize.thenMany(publisher);
return (adapter != null) ? adapter.fromPublisher(result) : result;
}
Mono<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Mono<?> result = preAuthorize.then(publisher);
return (adapter != null) ? adapter.fromPublisher(result) : result;
}
private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
if (Flux.class.isAssignableFrom(returnType)) {
return true;
}
return adapter == null || adapter.isMultiValue();
}
@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;
}
}

View File

@ -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.Pointcut;
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<? extends Annotation>... annotations) {
ComposablePointcut pointcut = null;

View File

@ -16,24 +16,18 @@
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.authorization.ExpressionAuthorizationDecision;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An {@link AuthorizationManager} which can determine if an {@link Authentication} may
@ -45,17 +39,14 @@ import org.springframework.util.Assert;
*/
public final class PostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
private final PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
/**
* 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 = new PostAuthorizeExpressionAttributeRegistry(expressionHandler);
}
/**
@ -73,36 +64,11 @@ public final class PostAuthorizeAuthorizationManager implements AuthorizationMan
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 ExpressionAuthorizationDecision(granted, attribute.getExpression());
}
private final class PostAuthorizeExpressionAttributeRegistry
extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
@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);
}
}
}

View File

@ -0,0 +1,72 @@
/*
* 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<ExpressionAttribute> {
private final MethodSecurityExpressionHandler expressionHandler;
PostAuthorizeExpressionAttributeRegistry() {
this(new DefaultMethodSecurityExpressionHandler());
}
PostAuthorizeExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
}
MethodSecurityExpressionHandler getExpressionHandler() {
return this.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);
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.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.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* 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<MethodInvocationResult> {
private final PostAuthorizeExpressionAttributeRegistry registry;
public PostAuthorizeReactiveAuthorizationManager() {
this(new DefaultMethodSecurityExpressionHandler());
}
public PostAuthorizeReactiveAuthorizationManager(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.registry = new PostAuthorizeExpressionAttributeRegistry(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<AuthorizationDecision> check(Mono<Authentication> 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
}
}

View File

@ -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.aopalliance.intercept.MethodInvocation;
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
@ -55,14 +49,12 @@ public final class PostFilterAuthorizationMethodInterceptor
private Supplier<Authentication> authentication = getAuthentication(
SecurityContextHolder.getContextHolderStrategy());
private final PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
private PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
private int order = AuthorizationInterceptorsOrder.POST_FILTER.getOrder();
private final Pointcut pointcut;
private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
/**
* Creates a {@link PostFilterAuthorizationMethodInterceptor} using the provided
* parameters
@ -76,8 +68,7 @@ public final class 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 = new PostFilterExpressionAttributeRegistry(expressionHandler);
}
/**
@ -133,8 +124,9 @@ public final class PostFilterAuthorizationMethodInterceptor
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<Authentication> getAuthentication(SecurityContextHolderStrategy strategy) {
@ -148,28 +140,4 @@ public final class PostFilterAuthorizationMethodInterceptor
};
}
private final class PostFilterExpressionAttributeRegistry
extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
@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);
}
}
}

View File

@ -0,0 +1,153 @@
/*
* 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.core.Ordered;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.util.Assert;
/**
* 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 {
private final PostFilterExpressionAttributeRegistry registry;
private final Pointcut pointcut = AuthorizationMethodPointcuts.forAnnotations(PostFilter.class);
private int order = AuthorizationInterceptorsOrder.POST_FILTER.getOrder();
/**
* Creates an instance.
*/
public PostFilterAuthorizationReactiveMethodInterceptor() {
this(new DefaultMethodSecurityExpressionHandler());
}
/**
* Creates an instance.
*/
public PostFilterAuthorizationReactiveMethodInterceptor(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.registry = new PostFilterExpressionAttributeRegistry(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 {
ExpressionAttribute attribute = this.registry.getAttribute(mi);
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return ReactiveMethodInvocationUtils.proceed(mi);
}
Mono<EvaluationContext> toInvoke = ReactiveAuthenticationUtils.getAuthentication()
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi));
Method method = mi.getMethod();
Class<?> type = method.getReturnType();
Assert.state(Publisher.class.isAssignableFrom(type),
() -> String.format("The parameter type %s on %s must be an instance of org.reactivestreams.Publisher "
+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
if (isMultiValue(type, adapter)) {
Publisher<?> publisher = Flux.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Flux<?> flux = toInvoke.flatMapMany((ctx) -> filterMultiValue(publisher, ctx, attribute));
return (adapter != null) ? adapter.fromPublisher(flux) : flux;
}
Publisher<?> publisher = Mono.defer(() -> ReactiveMethodInvocationUtils.proceed(mi));
Mono<?> mono = toInvoke.flatMap((ctx) -> filterSingleValue(publisher, ctx, attribute));
return (adapter != null) ? adapter.fromPublisher(mono) : mono;
}
private boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
if (Flux.class.isAssignableFrom(returnType)) {
return true;
}
return adapter == null || adapter.isMultiValue();
}
private Mono<?> filterSingleValue(Publisher<?> publisher, EvaluationContext ctx, ExpressionAttribute attribute) {
return Mono.from(publisher).doOnNext((result) -> setFilterObject(ctx, result))
.flatMap((result) -> postFilter(ctx, result, attribute));
}
private Flux<?> filterMultiValue(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;
}
}

View File

@ -0,0 +1,71 @@
/*
* 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<ExpressionAttribute> {
private final MethodSecurityExpressionHandler expressionHandler;
PostFilterExpressionAttributeRegistry() {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
}
PostFilterExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
}
MethodSecurityExpressionHandler getExpressionHandler() {
return this.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);
}
}

View File

@ -16,24 +16,18 @@
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.authorization.ExpressionAuthorizationDecision;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An {@link AuthorizationManager} which can determine if an {@link Authentication} may
@ -45,17 +39,14 @@ import org.springframework.util.Assert;
*/
public final class PreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
private final PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
/**
* 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 = new PreAuthorizeExpressionAttributeRegistry(expressionHandler);
}
/**
@ -73,33 +64,9 @@ public final class PreAuthorizeAuthorizationManager implements AuthorizationMana
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 ExpressionAuthorizationDecision(granted, attribute.getExpression());
}
private final class PreAuthorizeExpressionAttributeRegistry
extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
@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);
}
}
}

View File

@ -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<ExpressionAttribute> {
private final MethodSecurityExpressionHandler expressionHandler;
PreAuthorizeExpressionAttributeRegistry() {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
}
PreAuthorizeExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
}
/**
* Returns the {@link MethodSecurityExpressionHandler}.
* @return the {@link MethodSecurityExpressionHandler} to use
*/
MethodSecurityExpressionHandler getExpressionHandler() {
return this.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);
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.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.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* 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<MethodInvocation> {
private final PreAuthorizeExpressionAttributeRegistry registry;
public PreAuthorizeReactiveAuthorizationManager() {
this(new DefaultMethodSecurityExpressionHandler());
}
public PreAuthorizeReactiveAuthorizationManager(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.registry = new PreAuthorizeExpressionAttributeRegistry(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<AuthorizationDecision> check(Mono<Authentication> 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
}
}

View File

@ -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.aopalliance.intercept.MethodInvocation;
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;
@ -55,14 +50,12 @@ public final class PreFilterAuthorizationMethodInterceptor
private Supplier<Authentication> authentication = getAuthentication(
SecurityContextHolder.getContextHolderStrategy());
private final PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
private PreFilterExpressionAttributeRegistry registry = new PreFilterExpressionAttributeRegistry();
private int order = AuthorizationInterceptorsOrder.PRE_FILTER.getOrder();
private final Pointcut pointcut;
private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
/**
* Creates a {@link PreFilterAuthorizationMethodInterceptor} using the provided
* parameters
@ -76,8 +69,7 @@ public final class 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 = new PreFilterExpressionAttributeRegistry(expressionHandler);
}
/**
@ -127,13 +119,14 @@ public final class PreFilterAuthorizationMethodInterceptor
*/
@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 @@ public final class PreFilterAuthorizationMethodInterceptor
};
}
private final class PreFilterExpressionAttributeRegistry
extends AbstractExpressionAttributeRegistry<PreFilterExpressionAttribute> {
@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;
}
}
}

View File

@ -0,0 +1,213 @@
/*
* 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.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.DefaultMethodSecurityExpressionHandler;
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 {
private final PreFilterExpressionAttributeRegistry registry;
private final Pointcut pointcut = AuthorizationMethodPointcuts.forAnnotations(PreFilter.class);
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultSecurityParameterNameDiscoverer();
private int order = AuthorizationInterceptorsOrder.PRE_FILTER.getOrder();
public PreFilterAuthorizationReactiveMethodInterceptor() {
this(new DefaultMethodSecurityExpressionHandler());
}
/**
* Creates an instance.
*/
public PreFilterAuthorizationReactiveMethodInterceptor(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.registry = new PreFilterExpressionAttributeRegistry(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<EvaluationContext> toInvoke = ReactiveAuthenticationUtils.getAuthentication()
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi));
Method method = mi.getMethod();
Class<?> type = filterTarget.value.getClass();
Assert.state(Publisher.class.isAssignableFrom(type),
() -> String.format("The parameter type %s on %s must be an instance of org.reactivestreams.Publisher "
+ "(for example, a Mono or Flux) in order to support Reactor Context", type, method));
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(type);
if (isMultiValue(type, adapter)) {
Flux<?> result = toInvoke
.flatMapMany((ctx) -> filterMultiValue(filterTarget.value, attribute.getExpression(), ctx));
mi.getArguments()[filterTarget.index] = (adapter != null) ? adapter.fromPublisher(result) : result;
}
else {
Mono<?> result = toInvoke
.flatMap((ctx) -> filterSingleValue(filterTarget.value, attribute.getExpression(), ctx));
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 boolean isMultiValue(Class<?> returnType, ReactiveAdapter adapter) {
if (Flux.class.isAssignableFrom(returnType)) {
return true;
}
return adapter == null || adapter.isMultiValue();
}
private Mono<?> filterSingleValue(Publisher<?> filterTarget, Expression filterExpression, EvaluationContext ctx) {
MethodSecurityExpressionOperations rootObject = (MethodSecurityExpressionOperations) ctx.getRootObject()
.getValue();
return Mono.from(filterTarget).filterWhen((filterObject) -> {
rootObject.setFilterObject(filterObject);
return ReactiveExpressionUtils.evaluateAsBoolean(filterExpression, ctx);
});
}
private Flux<?> filterMultiValue(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;
}
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;
}
}
}

View File

@ -0,0 +1,89 @@
/*
* 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<PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute> {
private final MethodSecurityExpressionHandler expressionHandler;
PreFilterExpressionAttributeRegistry() {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
}
PreFilterExpressionAttributeRegistry(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
}
MethodSecurityExpressionHandler getExpressionHandler() {
return this.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;
}
}
}

View File

@ -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<Authentication> getAuthentication() {
return ReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication)
.defaultIfEmpty(ANONYMOUS);
}
private ReactiveAuthenticationUtils() {
}
}

View File

@ -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<Boolean> 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<Boolean> createInvalidReturnTypeMono(Expression expr) {
return Mono.error(() -> new IllegalStateException(
"Expression: '" + expr.getExpressionString() + "' must return boolean or Mono<Boolean>"));
}
private ReactiveExpressionUtils() {
}
}

View File

@ -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> T proceed(MethodInvocation mi) {
try {
return (T) mi.proceed();
}
catch (Throwable ex) {
throw Exceptions.propagate(ex);
}
}
private ReactiveMethodInvocationUtils() {
}
}

View File

@ -38,10 +38,15 @@ public class MockMethodInvocation implements MethodInvocation {
public MockMethodInvocation(Object targetObject, Class clazz, String methodName, Class... parameterTypes)
throws NoSuchMethodException {
this.method = clazz.getMethod(methodName, parameterTypes);
this(targetObject, clazz.getMethod(methodName, parameterTypes));
this.targetObject = targetObject;
}
public MockMethodInvocation(Object targetObject, Method method) {
this.targetObject = targetObject;
this.method = method;
}
@Override
public Object[] getArguments() {
return this.arguments;

View File

@ -0,0 +1,124 @@
/*
* 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.access.intercept.method.MockMethodInvocation;
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.spy;
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 = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> 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 = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocationResult> 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 = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocationResult> 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());
}
class Sample {
Mono<String> mono() {
return Mono.just("john");
}
Flux<String> flux() {
return Flux.just("john", "bob");
}
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.access.intercept.method.MockMethodInvocation;
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.spy;
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 = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> 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 = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("flux")));
given(mockMethodInvocation.proceed()).willReturn(Flux.just("john", "bob"));
ReactiveAuthorizationManager<MethodInvocation> 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 = spy(
new MockMethodInvocation(new Sample(), Sample.class.getDeclaredMethod("mono")));
given(mockMethodInvocation.proceed()).willReturn(Mono.just("john"));
ReactiveAuthorizationManager<MethodInvocation> 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));
}
class Sample {
Mono<String> mono() {
return Mono.just("john");
}
Flux<String> flux() {
return Flux.just("john", "bob");
}
}
}

View File

@ -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 class PostAuthorizeAuthorizationManagerTests {
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

View File

@ -0,0 +1,246 @@
/*
* 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(
expressionHandler);
assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
}
@Test
public void setExpressionHandlerWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> new PostAuthorizeReactiveAuthorizationManager(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<String> 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<String> 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> 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> 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> 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> 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<String> doSomethingList(List<String> 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 {
}
}

View File

@ -67,7 +67,7 @@ public class PostFilterAuthorizationMethodInterceptorTests {
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

View File

@ -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(
expressionHandler);
assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
}
@Test
public void setExpressionHandlerWhenNullThenException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new PostFilterAuthorizationReactiveMethodInterceptor(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<String> 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<String> 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<String> doSomethingFlux(Flux<String> flux) {
return flux;
}
public Flux<String> doSomethingFluxClassLevel(Flux<String> flux) {
return flux;
}
@PostFilter("filterObject == 'john'")
public Mono<String> doSomethingMono(Mono<String> 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 {
}
}

View File

@ -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 class PreAuthorizeAuthorizationManagerTests {
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

View File

@ -0,0 +1,210 @@
/*
* 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(
expressionHandler);
assertThat(manager).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
}
@Test
public void setExpressionHandlerWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> new PreAuthorizeReactiveAuthorizationManager(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> 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> 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> 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> 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 {
}
}

View File

@ -69,7 +69,7 @@ public class PreFilterAuthorizationMethodInterceptorTests {
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

View File

@ -0,0 +1,236 @@
/*
* 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(
expressionHandler);
assertThat(interceptor).extracting("registry").extracting("expressionHandler").isEqualTo(expressionHandler);
}
@Test
public void setExpressionHandlerWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> new PreFilterAuthorizationReactiveMethodInterceptor(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<String> doSomethingFluxFilterTargetNotMatch(Flux<String> flux) {
return flux;
}
@PreFilter(value = "filterObject == 'john'", filterTarget = "flux")
public Flux<String> doSomethingFluxFilterTargetMatch(Flux<String> flux) {
return flux;
}
@PreFilter("filterObject == 'john'")
public Flux<String> doSomethingFluxFilterTargetNotProvided(Flux<String> flux) {
return flux;
}
@PreFilter("filterObject == 'john'")
public Mono<String> doSomethingMonoFilterTargetNotProvided(Mono<String> mono) {
return mono;
}
public Flux<String> doSomethingTwoArgsFilterTargetNotProvided(String s, Flux<String> 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 {
}
}

View File

@ -6,10 +6,291 @@ The following example shows how to retrieve the currently logged in user's messa
[NOTE]
====
For this example to work, the return type of the method must be a `org.reactivestreams.Publisher` (that is, a `Mono` or a `Flux`) or the function must be a Kotlin coroutine function.
For this example to work, the return type of the method must be a `org.reactivestreams.Publisher` (that is, a `Mono` or a `Flux`).
This is necessary to integrate with Reactor's `Context`.
====
[[jc-enable-reactive-method-security-authorization-manager]]
== EnableReactiveMethodSecurity with AuthorizationManager
In Spring Security 5.8, we can enable annotation-based security using the `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` annotation on any `@Configuration` instance.
This improves upon `@EnableReactiveMethodSecurity` in a number of ways. `@EnableReactiveMethodSecurity(useAuthorizationManager=true)`:
1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
This simplifies reuse and customization.
2. Supports reactive return types. Note that we are waiting on https://github.com/spring-projects/spring-framework/issues/22462[additional coroutine support from the Spring Framework] before adding coroutine support.
3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize
4. Checks for conflicting annotations to ensure an unambiguous security configuration
5. Complies with JSR-250
[NOTE]
====
For earlier versions, please read about similar support with <<jc-enable-reactive-method-security, @EnableReactiveMethodSecurity>>.
====
For example, the following would enable Spring Security's `@PreAuthorize` annotation:
.Method Security Configuration
====
.Java
[source,java,role="primary"]
----
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
public class MethodSecurityConfig {
// ...
}
----
====
Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
Spring Security's native annotation support defines a set of attributes for the method.
These will be passed to the various method interceptors, like `AuthorizationManagerBeforeReactiveMethodInterceptor`, for it to make the actual decision:
.Method Security Annotation Usage
====
.Java
[source,java,role="primary"]
----
public interface BankService {
@PreAuthorize("hasRole('USER')")
Mono<Account> readAccount(Long id);
@PreAuthorize("hasRole('USER')")
Flux<Account> findAccounts();
@PreAuthorize("@func.apply(#account)")
Mono<Account> post(Account account, Double amount);
}
----
====
In this case `hasRole` refers to the method found in `SecurityExpressionRoot` that gets invoked by the SpEL evaluation engine.
`@bean` refers to a custom component you have defined, where `apply` can return `Boolean` or `Mono<Boolean>` to indicate the authorization decision.
A bean like that might look something like this:
.Method Security Reactive Boolean Expression
====
.Java
[source,java,role="primary"]
----
@Bean
public Function<Account, Mono<Boolean>> func() {
return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
}
----
====
=== Customizing Authorization
Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support.
[[jc-reactive-method-security-custom-granted-authority-defaults]]
Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`.
You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so:
.Custom MethodSecurityExpressionHandler
====
.Java
[source,java,role="primary"]
----
@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
----
====
[TIP]
====
We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
====
[[jc-reactive-method-security-custom-authorization-manager]]
=== Custom Authorization Managers
Method authorization is a combination of before- and after-method authorization.
[NOTE]
====
Before-method authorization is performed before the method is invoked.
If that authorization denies access, the method is not invoked, and an `AccessDeniedException` is thrown.
After-method authorization is performed after the method is invoked, but before the method returns to the caller.
If that authorization denies access, the value is not returned, and an `AccessDeniedException` is thrown
====
To recreate what adding `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` does by default, you would publish the following configuration:
.Full Pre-post Method Security Configuration
====
.Java
[source,java,role="primary"]
----
@Configuration
class MethodSecurityConfig {
@Bean
BeanDefinitionRegistryPostProcessor aopConfig() {
return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor() {
return new PreFilterAuthorizationReactiveMethodInterceptor();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor() {
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor() {
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor() {
return new PostFilterAuthorizationReactiveMethodInterceptor();
}
}
----
====
Notice that Spring Security's method security is built using Spring AOP.
So, interceptors are invoked based on the order specified.
This can be customized by calling `setOrder` on the interceptor instances like so:
.Publish Custom Advisor
====
.Java
[source,java,role="primary"]
----
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilterAuthorizationMethodInterceptor() {
PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
return interceptor;
}
----
====
You may want to only support `@PreAuthorize` in your application, in which case you can do the following:
.Only @PreAuthorize Configuration
====
.Java
[source,java,role="primary"]
----
@Configuration
class MethodSecurityConfig {
@Bean
BeanDefinitionRegistryPostProcessor aopConfig() {
return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}
}
----
====
Or, you may have a custom before-method `ReactiveAuthorizationManager` that you want to add to the list.
In this case, you will need to tell Spring Security both the `ReactiveAuthorizationManager` and to which methods and classes your authorization manager applies.
Thus, you can configure Spring Security to invoke your `ReactiveAuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so:
.Custom Before Advisor
====
.Java
[source,java,role="primary"]
----
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor customAuthorize() {
JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
pattern.setPattern("org.mycompany.myapp.service.*");
ReactiveAuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pattern, rule);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}
----
====
[TIP]
====
You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
====
The same can be done for after-method authorization.
After-method authorization is generally concerned with analysing the return value to verify access.
For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:
.@PostAuthorize example
====
.Java
[source,java,role="primary"]
----
public interface BankService {
@PreAuthorize("hasRole('USER')")
@PostAuthorize("returnObject.owner == authentication.name")
Mono<Account> readAccount(Long id);
}
----
====
You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated.
For example, if you have your own custom annotation, you can configure it like so:
.Custom After Advisor
====
.Java
[source,java,role="primary"]
----
@EnableReactiveMethodSecurity(useAuthorizationManager=true)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor customAuthorize(ReactiveAuthorizationManager<MethodInvocationResult> rules) {
AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(pattern, rules);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}
----
====
and it will be invoked after the `@PostAuthorize` interceptor.
== EnableReactiveMethodSecurity
[WARNING]
====
`@EnableReactiveMethodSecurity` also supports Kotlin coroutines, though only to a limited degree.
When intercepting coroutines, only the first interceptor participates.
If any other interceptors are present and come after Spring Security's method security interceptor, https://github.com/spring-projects/spring-framework/issues/22462[they will be skipped].
====
====
.Java
[source,java,role="primary"]