|
18 | 18 |
|
19 | 19 | import java.lang.annotation.Retention;
|
20 | 20 | 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; |
21 | 25 |
|
22 | 26 | import org.junit.jupiter.api.Test;
|
23 | 27 | import org.junit.jupiter.api.extension.ExtendWith;
|
|
34 | 38 | import org.springframework.security.access.AccessDeniedException;
|
35 | 39 | import org.springframework.security.access.PermissionEvaluator;
|
36 | 40 | 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; |
37 | 44 | import org.springframework.security.access.prepost.PostAuthorize;
|
38 | 45 | import org.springframework.security.access.prepost.PostFilter;
|
39 | 46 | import org.springframework.security.access.prepost.PreAuthorize;
|
40 | 47 | import org.springframework.security.access.prepost.PreFilter;
|
41 | 48 | import org.springframework.security.authorization.AuthorizationDeniedException;
|
| 49 | +import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory; |
| 50 | +import org.springframework.security.authorization.method.AuthorizeReturnObject; |
42 | 51 | import org.springframework.security.authorization.method.PrePostTemplateDefaults;
|
| 52 | +import org.springframework.security.config.Customizer; |
43 | 53 | import org.springframework.security.config.test.SpringTestContext;
|
44 | 54 | import org.springframework.security.config.test.SpringTestContextExtension;
|
45 | 55 | import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
|
|
49 | 59 |
|
50 | 60 | import static org.assertj.core.api.Assertions.assertThat;
|
51 | 61 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
| 62 | +import static org.assertj.core.api.Assertions.assertThatNoException; |
52 | 63 | import static org.mockito.ArgumentMatchers.any;
|
53 | 64 | import static org.mockito.ArgumentMatchers.eq;
|
54 | 65 | import static org.mockito.BDDMockito.given;
|
@@ -320,6 +331,84 @@ public void methodWhenPostFilterMetaAnnotationThenFilters(Class<?> config) {
|
320 | 331 | .containsExactly("dave");
|
321 | 332 | }
|
322 | 333 |
|
| 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 | + |
323 | 412 | @Configuration
|
324 | 413 | @EnableReactiveMethodSecurity
|
325 | 414 | static class MethodSecurityServiceEnabledConfig {
|
@@ -484,4 +573,137 @@ static class EntityClass {
|
484 | 573 |
|
485 | 574 | }
|
486 | 575 |
|
| 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 | + |
487 | 709 | }
|
0 commit comments