From 91053c58b51710e0056a4afc2dfa9fd0b79eaf89 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 12 Sep 2024 15:32:21 -0600 Subject: [PATCH] Test meta-annotation parameter support in Reactive Issue gh-14480 --- ...ctiveMethodSecurityConfigurationTests.java | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java index c6d0afb3b18..fed48d08f46 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java +++ b/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; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + import org.junit.jupiter.api.Test; 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 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.AccessDeniedException; import org.springframework.security.access.PermissionEvaluator; 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.method.PrePostTemplateDefaults; import org.springframework.security.config.test.SpringTestContext; 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.support.WithMockUser; 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.eq; import static org.mockito.BDDMockito.given; @@ -228,6 +244,82 @@ public void preAuthorizeWhenCustomMethodSecurityExpressionHandlerThenUses() { 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 @EnableReactiveMethodSecurity static class MethodSecurityServiceEnabledConfig { @@ -258,4 +350,138 @@ static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler( } + @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 hasRole(String role) { + return Mono.just(true); + } + + @RequireRole(role = "'USER'") + Mono hasUserRole() { + return Mono.just(true); + } + + @PreAuthorize("hasRole({role})") + Mono placeholdersOnlyResolvedByMetaAnnotations() { + return Mono.empty(); + } + + @HasClaim(claim = "message:read", roles = { "'ADMIN'" }) + Mono readMessage() { + return Mono.just("message"); + } + + @ResultStartsWith("dave") + Mono startsWithDave(String value) { + return Mono.just(value); + } + + @ParameterContains("dave") + Flux parametersContainDave(Flux list) { + return list; + } + + @ResultContains("dave") + Flux resultsContainDave(Flux list) { + return list; + } + + @RestrictedAccess(entityClass = EntityClass.class) + Mono 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(); + + } + }