Skip to content

Commit fee5dd3

Browse files
committed
Test AuthorizeReturnObject in Reactive
Issue gh-14597
1 parent fc2ad34 commit fee5dd3

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostReactiveMethodSecurityConfigurationTests.java

+222
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818

1919
import java.lang.annotation.Retention;
2020
import java.lang.annotation.RetentionPolicy;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.concurrent.ConcurrentHashMap;
2125

2226
import org.junit.jupiter.api.Test;
2327
import org.junit.jupiter.api.extension.ExtendWith;
@@ -34,12 +38,18 @@
3438
import org.springframework.security.access.AccessDeniedException;
3539
import org.springframework.security.access.PermissionEvaluator;
3640
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
41+
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
42+
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
43+
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
3744
import org.springframework.security.access.prepost.PostAuthorize;
3845
import org.springframework.security.access.prepost.PostFilter;
3946
import org.springframework.security.access.prepost.PreAuthorize;
4047
import org.springframework.security.access.prepost.PreFilter;
4148
import org.springframework.security.authorization.AuthorizationDeniedException;
49+
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
50+
import org.springframework.security.authorization.method.AuthorizeReturnObject;
4251
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
52+
import org.springframework.security.config.Customizer;
4353
import org.springframework.security.config.test.SpringTestContext;
4454
import org.springframework.security.config.test.SpringTestContextExtension;
4555
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
@@ -49,6 +59,7 @@
4959

5060
import static org.assertj.core.api.Assertions.assertThat;
5161
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
62+
import static org.assertj.core.api.Assertions.assertThatNoException;
5263
import static org.mockito.ArgumentMatchers.any;
5364
import static org.mockito.ArgumentMatchers.eq;
5465
import static org.mockito.BDDMockito.given;
@@ -320,6 +331,84 @@ public void methodWhenPostFilterMetaAnnotationThenFilters(Class<?> config) {
320331
.containsExactly("dave");
321332
}
322333

334+
@Test
335+
@WithMockUser(authorities = "airplane:read")
336+
public void findByIdWhenAuthorizedResultThenAuthorizes() {
337+
this.spring.register(AuthorizeResultConfig.class).autowire();
338+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
339+
Flight flight = flights.findById("1").block();
340+
assertThatNoException().isThrownBy(flight::getAltitude);
341+
assertThatNoException().isThrownBy(flight::getSeats);
342+
}
343+
344+
@Test
345+
@WithMockUser(authorities = "seating:read")
346+
public void findByIdWhenUnauthorizedResultThenDenies() {
347+
this.spring.register(AuthorizeResultConfig.class).autowire();
348+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
349+
Flight flight = flights.findById("1").block();
350+
assertThatNoException().isThrownBy(flight::getSeats);
351+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
352+
}
353+
354+
@Test
355+
@WithMockUser(authorities = "seating:read")
356+
public void findAllWhenUnauthorizedResultThenDenies() {
357+
this.spring.register(AuthorizeResultConfig.class).autowire();
358+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
359+
flights.findAll().collectList().block().forEach((flight) -> {
360+
assertThatNoException().isThrownBy(flight::getSeats);
361+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
362+
});
363+
}
364+
365+
@Test
366+
public void removeWhenAuthorizedResultThenRemoves() {
367+
this.spring.register(AuthorizeResultConfig.class).autowire();
368+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
369+
flights.remove("1");
370+
}
371+
372+
@Test
373+
@WithMockUser(authorities = "airplane:read")
374+
public void findAllWhenPostFilterThenFilters() {
375+
this.spring.register(AuthorizeResultConfig.class).autowire();
376+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
377+
flights.findAll()
378+
.collectList()
379+
.block()
380+
.forEach((flight) -> assertThat(flight.getPassengers().collectList().block())
381+
.extracting((p) -> p.getName().block())
382+
.doesNotContain("Kevin Mitnick"));
383+
}
384+
385+
@Test
386+
@WithMockUser(authorities = "airplane:read")
387+
public void findAllWhenPreFilterThenFilters() {
388+
this.spring.register(AuthorizeResultConfig.class).autowire();
389+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
390+
flights.findAll().collectList().block().forEach((flight) -> {
391+
flight.board(Flux.just("John")).block();
392+
assertThat(flight.getPassengers().collectList().block()).extracting((p) -> p.getName().block())
393+
.doesNotContain("John");
394+
flight.board(Flux.just("John Doe")).block();
395+
assertThat(flight.getPassengers().collectList().block()).extracting((p) -> p.getName().block())
396+
.contains("John Doe");
397+
});
398+
}
399+
400+
@Test
401+
@WithMockUser(authorities = "seating:read")
402+
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
403+
this.spring.register(AuthorizeResultConfig.class).autowire();
404+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
405+
flights.findAll().collectList().block().forEach((flight) -> {
406+
List<Passenger> passengers = flight.getPassengers().collectList().block();
407+
passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class)
408+
.isThrownBy(() -> passenger.getName().block()));
409+
});
410+
}
411+
323412
@Configuration
324413
@EnableReactiveMethodSecurity
325414
static class MethodSecurityServiceEnabledConfig {
@@ -484,4 +573,137 @@ static class EntityClass {
484573

485574
}
486575

576+
@EnableReactiveMethodSecurity
577+
@Configuration
578+
public static class AuthorizeResultConfig {
579+
580+
@Bean
581+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
582+
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
583+
return (f) -> f.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.defaultsSkipValueTypes());
584+
}
585+
586+
@Bean
587+
FlightRepository flights() {
588+
FlightRepository flights = new FlightRepository();
589+
Flight one = new Flight("1", 35000d, 35);
590+
one.board(Flux.just("Marie Curie", "Kevin Mitnick", "Ada Lovelace")).block();
591+
flights.save(one).block();
592+
Flight two = new Flight("2", 32000d, 72);
593+
two.board(Flux.just("Albert Einstein")).block();
594+
flights.save(two).block();
595+
return flights;
596+
}
597+
598+
@Bean
599+
static MethodSecurityExpressionHandler expressionHandler() {
600+
RoleHierarchy hierarchy = RoleHierarchyImpl.withRolePrefix("")
601+
.role("airplane:read")
602+
.implies("seating:read")
603+
.build();
604+
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
605+
expressionHandler.setRoleHierarchy(hierarchy);
606+
return expressionHandler;
607+
}
608+
609+
@Bean
610+
Authz authz() {
611+
return new Authz();
612+
}
613+
614+
public static class Authz {
615+
616+
public Mono<Boolean> isNotKevinMitnick(Passenger passenger) {
617+
return passenger.getName().map((n) -> !"Kevin Mitnick".equals(n));
618+
}
619+
620+
}
621+
622+
}
623+
624+
@AuthorizeReturnObject
625+
static class FlightRepository {
626+
627+
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
628+
629+
Flux<Flight> findAll() {
630+
return Flux.fromIterable(this.flights.values());
631+
}
632+
633+
Mono<Flight> findById(String id) {
634+
return Mono.just(this.flights.get(id));
635+
}
636+
637+
Mono<Flight> save(Flight flight) {
638+
this.flights.put(flight.getId(), flight);
639+
return Mono.just(flight);
640+
}
641+
642+
Mono<Void> remove(String id) {
643+
this.flights.remove(id);
644+
return Mono.empty();
645+
}
646+
647+
}
648+
649+
@AuthorizeReturnObject
650+
static class Flight {
651+
652+
private final String id;
653+
654+
private final Double altitude;
655+
656+
private final Integer seats;
657+
658+
private final List<Passenger> passengers = new ArrayList<>();
659+
660+
Flight(String id, Double altitude, Integer seats) {
661+
this.id = id;
662+
this.altitude = altitude;
663+
this.seats = seats;
664+
}
665+
666+
String getId() {
667+
return this.id;
668+
}
669+
670+
@PreAuthorize("hasAuthority('airplane:read')")
671+
Mono<Double> getAltitude() {
672+
return Mono.just(this.altitude);
673+
}
674+
675+
@PreAuthorize("hasAuthority('seating:read')")
676+
Mono<Integer> getSeats() {
677+
return Mono.just(this.seats);
678+
}
679+
680+
@PostAuthorize("hasAuthority('seating:read')")
681+
@PostFilter("@authz.isNotKevinMitnick(filterObject)")
682+
Flux<Passenger> getPassengers() {
683+
return Flux.fromIterable(this.passengers);
684+
}
685+
686+
@PreAuthorize("hasAuthority('seating:read')")
687+
@PreFilter("filterObject.contains(' ')")
688+
Mono<Void> board(Flux<String> passengers) {
689+
return passengers.doOnNext((passenger) -> this.passengers.add(new Passenger(passenger))).then(Mono.empty());
690+
}
691+
692+
}
693+
694+
public static class Passenger {
695+
696+
String name;
697+
698+
public Passenger(String name) {
699+
this.name = name;
700+
}
701+
702+
@PreAuthorize("hasAuthority('airplane:read')")
703+
public Mono<String> getName() {
704+
return Mono.just(this.name);
705+
}
706+
707+
}
708+
487709
}

0 commit comments

Comments
 (0)