Skip to content

Commit 3b46db4

Browse files
committed
Add AuthorizeReturnObject
Closes spring-projectsgh-14597
1 parent c611b7e commit 3b46db4

File tree

14 files changed

+675
-6
lines changed

14 files changed

+675
-6
lines changed

config/src/main/java/org/springframework/security/config/annotation/method/configuration/AuthorizationProxyConfiguration.java

+16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121

22+
import org.aopalliance.intercept.MethodInterceptor;
23+
2224
import org.springframework.aop.framework.AopInfrastructureBean;
2325
import org.springframework.beans.factory.ObjectProvider;
2426
import org.springframework.beans.factory.config.BeanDefinition;
@@ -27,6 +29,7 @@
2729
import org.springframework.context.annotation.Role;
2830
import org.springframework.security.authorization.AuthorizationAdvisorProxyFactory;
2931
import org.springframework.security.authorization.method.AuthorizationAdvisor;
32+
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
3033

3134
@Configuration(proxyBeanMethods = false)
3235
final class AuthorizationProxyConfiguration implements AopInfrastructureBean {
@@ -41,4 +44,17 @@ static AuthorizationAdvisorProxyFactory authorizationProxyFactory(ObjectProvider
4144
return factory;
4245
}
4346

47+
@Bean
48+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
49+
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
50+
AuthorizationAdvisorProxyFactory authorizationProxyFactory) {
51+
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
52+
authorizationProxyFactory);
53+
List<AuthorizationAdvisor> advisors = new ArrayList<>();
54+
provider.forEach(advisors::add);
55+
advisors.add(interceptor);
56+
authorizationProxyFactory.setAdvisors(advisors);
57+
return interceptor;
58+
}
59+
4460
}

config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityAdvisorRegistrar.java

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B
3333
registerAsAdvisor("postAuthorizeAuthorization", registry);
3434
registerAsAdvisor("securedAuthorization", registry);
3535
registerAsAdvisor("jsr250Authorization", registry);
36+
registerAsAdvisor("authorizeReturnObject", registry);
3637
}
3738

3839
private void registerAsAdvisor(String prefix, BeanDefinitionRegistry registry) {

config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveAuthorizationProxyConfiguration.java

+16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121

22+
import org.aopalliance.intercept.MethodInterceptor;
23+
2224
import org.springframework.aop.framework.AopInfrastructureBean;
2325
import org.springframework.beans.factory.ObjectProvider;
2426
import org.springframework.beans.factory.config.BeanDefinition;
@@ -27,6 +29,7 @@
2729
import org.springframework.context.annotation.Role;
2830
import org.springframework.security.authorization.ReactiveAuthorizationAdvisorProxyFactory;
2931
import org.springframework.security.authorization.method.AuthorizationAdvisor;
32+
import org.springframework.security.authorization.method.AuthorizeReturnObjectMethodInterceptor;
3033

3134
@Configuration(proxyBeanMethods = false)
3235
final class ReactiveAuthorizationProxyConfiguration implements AopInfrastructureBean {
@@ -42,4 +45,17 @@ static ReactiveAuthorizationAdvisorProxyFactory authorizationProxyFactory(
4245
return factory;
4346
}
4447

48+
@Bean
49+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
50+
static MethodInterceptor authorizeReturnObjectMethodInterceptor(ObjectProvider<AuthorizationAdvisor> provider,
51+
ReactiveAuthorizationAdvisorProxyFactory authorizationProxyFactory) {
52+
AuthorizeReturnObjectMethodInterceptor interceptor = new AuthorizeReturnObjectMethodInterceptor(
53+
authorizationProxyFactory);
54+
List<AuthorizationAdvisor> advisors = new ArrayList<>();
55+
provider.forEach(advisors::add);
56+
advisors.add(interceptor);
57+
authorizationProxyFactory.setAdvisors(advisors);
58+
return interceptor;
59+
}
60+
4561
}

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

+187
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.util.ArrayList;
2323
import java.util.Arrays;
24+
import java.util.Iterator;
2425
import java.util.List;
26+
import java.util.Map;
27+
import java.util.concurrent.ConcurrentHashMap;
2528
import java.util.function.Consumer;
2629
import java.util.function.Supplier;
2730

@@ -60,6 +63,7 @@
6063
import org.springframework.security.authorization.AuthorizationManager;
6164
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
6265
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
66+
import org.springframework.security.authorization.method.AuthorizeReturnObject;
6367
import org.springframework.security.authorization.method.MethodInvocationResult;
6468
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
6569
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
@@ -80,6 +84,7 @@
8084

8185
import static org.assertj.core.api.Assertions.assertThat;
8286
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
87+
import static org.assertj.core.api.Assertions.assertThatNoException;
8388
import static org.mockito.ArgumentMatchers.any;
8489
import static org.mockito.Mockito.atLeastOnce;
8590
import static org.mockito.Mockito.mock;
@@ -662,6 +667,79 @@ public void methodWhenPostFilterMetaAnnotationThenFilters() {
662667
.containsExactly("dave");
663668
}
664669

670+
@Test
671+
@WithMockUser(authorities = "airplane:read")
672+
public void findByIdWhenAuthorizedResultThenAuthorizes() {
673+
this.spring.register(AuthorizeResultConfig.class).autowire();
674+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
675+
Flight flight = flights.findById("1");
676+
assertThatNoException().isThrownBy(flight::getAltitude);
677+
assertThatNoException().isThrownBy(flight::getSeats);
678+
}
679+
680+
@Test
681+
@WithMockUser(authorities = "seating:read")
682+
public void findByIdWhenUnauthorizedResultThenDenies() {
683+
this.spring.register(AuthorizeResultConfig.class).autowire();
684+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
685+
Flight flight = flights.findById("1");
686+
assertThatNoException().isThrownBy(flight::getSeats);
687+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
688+
}
689+
690+
@Test
691+
@WithMockUser(authorities = "seating:read")
692+
public void findAllWhenUnauthorizedResultThenDenies() {
693+
this.spring.register(AuthorizeResultConfig.class).autowire();
694+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
695+
flights.findAll().forEachRemaining((flight) -> {
696+
assertThatNoException().isThrownBy(flight::getSeats);
697+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
698+
});
699+
}
700+
701+
@Test
702+
public void removeWhenAuthorizedResultThenRemoves() {
703+
this.spring.register(AuthorizeResultConfig.class).autowire();
704+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
705+
flights.remove("1");
706+
}
707+
708+
@Test
709+
@WithMockUser(authorities = "airplane:read")
710+
public void findAllWhenPostFilterThenFilters() {
711+
this.spring.register(AuthorizeResultConfig.class).autowire();
712+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
713+
flights.findAll()
714+
.forEachRemaining((flight) -> assertThat(flight.getPassengers()).extracting(Passenger::getName)
715+
.doesNotContain("Kevin Mitnick"));
716+
}
717+
718+
@Test
719+
@WithMockUser(authorities = "airplane:read")
720+
public void findAllWhenPreFilterThenFilters() {
721+
this.spring.register(AuthorizeResultConfig.class).autowire();
722+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
723+
flights.findAll().forEachRemaining((flight) -> {
724+
flight.board(new ArrayList<>(List.of("John")));
725+
assertThat(flight.getPassengers()).extracting(Passenger::getName).doesNotContain("John");
726+
flight.board(new ArrayList<>(List.of("John Doe")));
727+
assertThat(flight.getPassengers()).extracting(Passenger::getName).contains("John Doe");
728+
});
729+
}
730+
731+
@Test
732+
@WithMockUser(authorities = "seating:read")
733+
public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
734+
this.spring.register(AuthorizeResultConfig.class).autowire();
735+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
736+
flights.findAll().forEachRemaining((flight) -> {
737+
List<Passenger> passengers = flight.getPassengers();
738+
passengers.forEach((passenger) -> assertThatExceptionOfType(AccessDeniedException.class)
739+
.isThrownBy(passenger::getName));
740+
});
741+
}
742+
665743
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
666744
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
667745
}
@@ -1061,4 +1139,113 @@ List<String> resultsContainDave(List<String> list) {
10611139

10621140
}
10631141

1142+
@EnableMethodSecurity
1143+
@Configuration
1144+
static class AuthorizeResultConfig {
1145+
1146+
@Bean
1147+
FlightRepository flights() {
1148+
FlightRepository flights = new FlightRepository();
1149+
Flight one = new Flight("1", 35000d, 35);
1150+
one.board(new ArrayList<>(List.of("Marie Curie", "Kevin Mitnick", "Ada Lovelace")));
1151+
flights.save(one);
1152+
Flight two = new Flight("2", 32000d, 72);
1153+
two.board(new ArrayList<>(List.of("Albert Einstein")));
1154+
flights.save(two);
1155+
return flights;
1156+
}
1157+
1158+
@Bean
1159+
RoleHierarchy roleHierarchy() {
1160+
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
1161+
}
1162+
1163+
}
1164+
1165+
@AuthorizeReturnObject
1166+
static class FlightRepository {
1167+
1168+
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
1169+
1170+
Iterator<Flight> findAll() {
1171+
return this.flights.values().iterator();
1172+
}
1173+
1174+
Flight findById(String id) {
1175+
return this.flights.get(id);
1176+
}
1177+
1178+
Flight save(Flight flight) {
1179+
this.flights.put(flight.getId(), flight);
1180+
return flight;
1181+
}
1182+
1183+
void remove(String id) {
1184+
this.flights.remove(id);
1185+
}
1186+
1187+
}
1188+
1189+
static class Flight {
1190+
1191+
private final String id;
1192+
1193+
private final Double altitude;
1194+
1195+
private final Integer seats;
1196+
1197+
private final List<Passenger> passengers = new ArrayList<>();
1198+
1199+
Flight(String id, Double altitude, Integer seats) {
1200+
this.id = id;
1201+
this.altitude = altitude;
1202+
this.seats = seats;
1203+
}
1204+
1205+
String getId() {
1206+
return this.id;
1207+
}
1208+
1209+
@PreAuthorize("hasAuthority('airplane:read')")
1210+
Double getAltitude() {
1211+
return this.altitude;
1212+
}
1213+
1214+
@PreAuthorize("hasAuthority('seating:read')")
1215+
Integer getSeats() {
1216+
return this.seats;
1217+
}
1218+
1219+
@AuthorizeReturnObject
1220+
@PostAuthorize("hasAuthority('seating:read')")
1221+
@PostFilter("filterObject.name != 'Kevin Mitnick'")
1222+
List<Passenger> getPassengers() {
1223+
return this.passengers;
1224+
}
1225+
1226+
@PreAuthorize("hasAuthority('seating:read')")
1227+
@PreFilter("filterObject.contains(' ')")
1228+
void board(List<String> passengers) {
1229+
for (String passenger : passengers) {
1230+
this.passengers.add(new Passenger(passenger));
1231+
}
1232+
}
1233+
1234+
}
1235+
1236+
public static class Passenger {
1237+
1238+
String name;
1239+
1240+
public Passenger(String name) {
1241+
this.name = name;
1242+
}
1243+
1244+
@PreAuthorize("hasAuthority('airplane:read')")
1245+
public String getName() {
1246+
return this.name;
1247+
}
1248+
1249+
}
1250+
10641251
}

0 commit comments

Comments
 (0)