Browse Source

Test meta-annotation parameter support in Reactive

Issue gh-14480
Josh Cummings 11 months ago
parent
commit
fc2ad34e5d

+ 226 - 0
config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java

@@ -16,23 +16,39 @@
 
 
 package org.springframework.security.config.annotation.method.configuration;
 package org.springframework.security.config.annotation.method.configuration;
 
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 import reactor.test.StepVerifier;
 
 
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Role;
 import org.springframework.context.annotation.Role;
+import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.PermissionEvaluator;
 import org.springframework.security.access.PermissionEvaluator;
 import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
 import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+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;
 import org.springframework.security.authorization.AuthorizationDeniedException;
 import org.springframework.security.authorization.AuthorizationDeniedException;
+import org.springframework.security.authorization.method.PrePostTemplateDefaults;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
 import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
 import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
 import org.springframework.security.test.context.support.WithMockUser;
 import org.springframework.security.test.context.support.WithMockUser;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.BDDMockito.given;
@@ -228,6 +244,82 @@ public class PrePostReactiveMethodSecurityConfigurationTests {
 		verify(permissionEvaluator, times(2)).hasPermission(any(), any(), any());
 		verify(permissionEvaluator, times(2)).hasPermission(any(), any(), any());
 	}
 	}
 
 
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	@WithMockUser
+	public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		assertThat(service.hasRole("USER").block()).isTrue();
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	@WithMockUser
+	public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		assertThat(service.hasUserRole().block()).isTrue();
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	public void methodWhenParameterizedAnnotationThenFails(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> service.placeholdersOnlyResolvedByMetaAnnotations().block());
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	@WithMockUser(authorities = "SCOPE_message:read")
+	public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		assertThat(service.readMessage().block()).isEqualTo("message");
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	@WithMockUser(roles = "ADMIN")
+	public void methodWhenMultiplePlaceholdersHasRoleThenPasses(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		assertThat(service.readMessage().block()).isEqualTo("message");
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	@WithMockUser
+	public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		service.startsWithDave("daveMatthews");
+		assertThatExceptionOfType(AccessDeniedException.class)
+			.isThrownBy(() -> service.startsWithDave("jenniferHarper").block());
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	@WithMockUser
+	public void methodWhenPreFilterMetaAnnotationThenFilters(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		assertThat(service.parametersContainDave(Flux.just("dave", "carla", "vanessa", "paul")).collectList().block())
+			.containsExactly("dave");
+	}
+
+	@ParameterizedTest
+	@ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class })
+	@WithMockUser
+	public void methodWhenPostFilterMetaAnnotationThenFilters(Class<?> config) {
+		this.spring.register(config).autowire();
+		MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
+		assertThat(service.resultsContainDave(Flux.just("dave", "carla", "vanessa", "paul")).collectList().block())
+			.containsExactly("dave");
+	}
+
 	@Configuration
 	@Configuration
 	@EnableReactiveMethodSecurity
 	@EnableReactiveMethodSecurity
 	static class MethodSecurityServiceEnabledConfig {
 	static class MethodSecurityServiceEnabledConfig {
@@ -258,4 +350,138 @@ public class PrePostReactiveMethodSecurityConfigurationTests {
 
 
 	}
 	}
 
 
+	@Configuration
+	@EnableReactiveMethodSecurity
+	static class LegacyMetaAnnotationPlaceholderConfig {
+
+		@Bean
+		PrePostTemplateDefaults methodSecurityDefaults() {
+			return new PrePostTemplateDefaults();
+		}
+
+		@Bean
+		MetaAnnotationService metaAnnotationService() {
+			return new MetaAnnotationService();
+		}
+
+	}
+
+	@Configuration
+	@EnableReactiveMethodSecurity
+	static class MetaAnnotationPlaceholderConfig {
+
+		@Bean
+		AnnotationTemplateExpressionDefaults methodSecurityDefaults() {
+			return new AnnotationTemplateExpressionDefaults();
+		}
+
+		@Bean
+		MetaAnnotationService metaAnnotationService() {
+			return new MetaAnnotationService();
+		}
+
+	}
+
+	static class MetaAnnotationService {
+
+		@RequireRole(role = "#role")
+		Mono<Boolean> hasRole(String role) {
+			return Mono.just(true);
+		}
+
+		@RequireRole(role = "'USER'")
+		Mono<Boolean> hasUserRole() {
+			return Mono.just(true);
+		}
+
+		@PreAuthorize("hasRole({role})")
+		Mono<Void> placeholdersOnlyResolvedByMetaAnnotations() {
+			return Mono.empty();
+		}
+
+		@HasClaim(claim = "message:read", roles = { "'ADMIN'" })
+		Mono<String> readMessage() {
+			return Mono.just("message");
+		}
+
+		@ResultStartsWith("dave")
+		Mono<String> startsWithDave(String value) {
+			return Mono.just(value);
+		}
+
+		@ParameterContains("dave")
+		Flux<String> parametersContainDave(Flux<String> list) {
+			return list;
+		}
+
+		@ResultContains("dave")
+		Flux<String> resultsContainDave(Flux<String> list) {
+			return list;
+		}
+
+		@RestrictedAccess(entityClass = EntityClass.class)
+		Mono<String> getIdPath(String id) {
+			return Mono.just(id);
+		}
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PreAuthorize("hasRole({idPath})")
+	@interface RestrictedAccess {
+
+		String idPath() default "#id";
+
+		Class<?> entityClass();
+
+		String[] recipes() default {};
+
+	}
+
+	static class EntityClass {
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PreAuthorize("hasRole({role})")
+	@interface RequireRole {
+
+		String role();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})")
+	@interface HasClaim {
+
+		String claim();
+
+		String[] roles() default {};
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PostAuthorize("returnObject.startsWith('{value}')")
+	@interface ResultStartsWith {
+
+		String value();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PreFilter("filterObject.contains('{value}')")
+	@interface ParameterContains {
+
+		String value();
+
+	}
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@PostFilter("filterObject.contains('{value}')")
+	@interface ResultContains {
+
+		String value();
+
+	}
+
 }
 }