Skip to content

Commit

Permalink
Test meta-annotation parameter support in Reactive
Browse files Browse the repository at this point in the history
  • Loading branch information
jzheaux committed Sep 12, 2024
1 parent 358e0c8 commit 91053c5
Showing 1 changed file with 226 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<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();

}

}

0 comments on commit 91053c5

Please sign in to comment.