|
@@ -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 class PrePostReactiveMethodSecurityConfigurationTests {
|
|
|
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 @@ 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();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
}
|