Skip to content

Commit 2f3457e

Browse files
committed
Add AuthorizationResult Auto-Proxy Support
Closes gh-14597
1 parent 6d290c9 commit 2f3457e

15 files changed

+315
-27
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.method.configuration;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.aopalliance.intercept.MethodInterceptor;
23+
24+
import org.springframework.aop.Advisor;
25+
import org.springframework.aop.framework.AopInfrastructureBean;
26+
import org.springframework.beans.factory.ObjectProvider;
27+
import org.springframework.beans.factory.config.BeanDefinition;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.context.annotation.Role;
31+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
32+
import org.springframework.security.authorization.method.AuthorizationAdvisor;
33+
import org.springframework.security.authorization.method.AuthorizationProxyMethodInterceptor;
34+
import org.springframework.security.authorization.object.AuthorizationProxyFactory;
35+
36+
@Configuration(proxyBeanMethods = false)
37+
public class AuthorizationProxyConfiguration implements AopInfrastructureBean {
38+
39+
@Bean
40+
static AuthorizationProxyFactory authorizationProxyFactory(ObjectProvider<AuthorizationAdvisor> provider) {
41+
List<Advisor> advisors = new ArrayList<>();
42+
provider.forEach(advisors::add);
43+
AnnotationAwareOrderComparator.sort(advisors);
44+
return new AuthorizationProxyFactory(advisors);
45+
}
46+
47+
@Bean
48+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
49+
static MethodInterceptor authorizationProxyMethodInterceptor(AuthorizationProxyFactory authorizationProxyFactory) {
50+
return new AuthorizationProxyMethodInterceptor(authorizationProxyFactory);
51+
}
52+
53+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.aopalliance.intercept.MethodInterceptor;
2121
import org.aopalliance.intercept.MethodInvocation;
2222

23+
import org.springframework.aop.framework.AopInfrastructureBean;
2324
import org.springframework.beans.factory.ObjectProvider;
2425
import org.springframework.beans.factory.config.BeanDefinition;
2526
import org.springframework.context.annotation.Bean;
@@ -48,7 +49,7 @@
4849
*/
4950
@Configuration(proxyBeanMethods = false)
5051
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
51-
final class Jsr250MethodSecurityConfiguration implements ImportAware {
52+
final class Jsr250MethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
5253

5354
private int interceptorOrderOffset;
5455

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

Lines changed: 1 addition & 0 deletions
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("authorizationProxy", registry);
3637
}
3738

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
5656
if (annotation.jsr250Enabled()) {
5757
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
5858
}
59+
imports.add(AuthorizationProxyConfiguration.class.getName());
5960
return imports.toArray(new String[0]);
6061
}
6162

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.jetbrains.annotations.Nullable;
2828

2929
import org.springframework.aop.Pointcut;
30-
import org.springframework.aop.PointcutAdvisor;
3130
import org.springframework.aop.framework.AopInfrastructureBean;
3231
import org.springframework.beans.factory.ObjectProvider;
3332
import org.springframework.beans.factory.config.BeanDefinition;
@@ -36,14 +35,14 @@
3635
import org.springframework.context.annotation.Configuration;
3736
import org.springframework.context.annotation.ImportAware;
3837
import org.springframework.context.annotation.Role;
39-
import org.springframework.core.Ordered;
4038
import org.springframework.core.type.AnnotationMetadata;
4139
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
4240
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
4341
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
4442
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
4543
import org.springframework.security.authorization.AuthorizationEventPublisher;
4644
import org.springframework.security.authorization.AuthorizationManager;
45+
import org.springframework.security.authorization.method.AuthorizationAdvisor;
4746
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
4847
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
4948
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
@@ -65,7 +64,7 @@
6564
*/
6665
@Configuration(proxyBeanMethods = false)
6766
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
68-
final class PrePostMethodSecurityConfiguration implements ImportAware {
67+
final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
6968

7069
private int interceptorOrderOffset;
7170

@@ -175,8 +174,8 @@ public void setImportMetadata(AnnotationMetadata importMetadata) {
175174
this.interceptorOrderOffset = annotation.offset();
176175
}
177176

178-
private static final class DeferringMethodInterceptor<M extends Ordered & MethodInterceptor & PointcutAdvisor>
179-
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
177+
private static final class DeferringMethodInterceptor<M extends AuthorizationAdvisor>
178+
implements AuthorizationAdvisor {
180179

181180
private final Pointcut pointcut;
182181

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.aopalliance.intercept.MethodInterceptor;
2121
import org.aopalliance.intercept.MethodInvocation;
2222

23+
import org.springframework.aop.framework.AopInfrastructureBean;
2324
import org.springframework.beans.factory.ObjectProvider;
2425
import org.springframework.beans.factory.config.BeanDefinition;
2526
import org.springframework.context.annotation.Bean;
@@ -48,7 +49,7 @@
4849
*/
4950
@Configuration(proxyBeanMethods = false)
5051
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
51-
final class SecuredMethodSecurityConfiguration implements ImportAware {
52+
final class SecuredMethodSecurityConfiguration implements ImportAware, AopInfrastructureBean {
5253

5354
private int interceptorOrderOffset;
5455

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

Lines changed: 115 additions & 0 deletions
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.AuthorizeResult;
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,44 @@ 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+
665708
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
666709
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
667710
}
@@ -1061,4 +1104,76 @@ List<String> resultsContainDave(List<String> list) {
10611104

10621105
}
10631106

1107+
@EnableMethodSecurity
1108+
static class AuthorizeResultConfig {
1109+
1110+
@Bean
1111+
FlightRepository flights() {
1112+
FlightRepository flights = new FlightRepository();
1113+
flights.save(new Flight("1", 35000d, 35));
1114+
flights.save(new Flight("2", 32000d, 72));
1115+
return flights;
1116+
}
1117+
1118+
@Bean
1119+
RoleHierarchy roleHierarchy() {
1120+
return RoleHierarchyImpl.withRolePrefix("").role("airplane:read").implies("seating:read").build();
1121+
}
1122+
1123+
}
1124+
1125+
@AuthorizeResult
1126+
static class FlightRepository {
1127+
1128+
private final Map<String, Flight> flights = new ConcurrentHashMap<>();
1129+
1130+
Iterator<Flight> findAll() {
1131+
return this.flights.values().iterator();
1132+
}
1133+
1134+
Flight findById(String id) {
1135+
return this.flights.get(id);
1136+
}
1137+
1138+
Flight save(Flight flight) {
1139+
this.flights.put(flight.getId(), flight);
1140+
return flight;
1141+
}
1142+
1143+
void remove(String id) {
1144+
this.flights.remove(id);
1145+
}
1146+
1147+
}
1148+
1149+
static class Flight {
1150+
1151+
private final String id;
1152+
1153+
private final Double altitude;
1154+
1155+
private final Integer seats;
1156+
1157+
Flight(String id, Double altitude, Integer seats) {
1158+
this.id = id;
1159+
this.altitude = altitude;
1160+
this.seats = seats;
1161+
}
1162+
1163+
String getId() {
1164+
return this.id;
1165+
}
1166+
1167+
@PreAuthorize("hasAuthority('airplane:read')")
1168+
Double getAltitude() {
1169+
return this.altitude;
1170+
}
1171+
1172+
@PreAuthorize("hasAuthority('seating:read')")
1173+
Integer getSeats() {
1174+
return this.seats;
1175+
}
1176+
1177+
}
1178+
10641179
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authorization.method;
18+
19+
import org.aopalliance.intercept.MethodInterceptor;
20+
21+
import org.springframework.aop.PointcutAdvisor;
22+
import org.springframework.aop.framework.AopInfrastructureBean;
23+
import org.springframework.core.Ordered;
24+
25+
public interface AuthorizationAdvisor extends Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
26+
27+
}

core/src/main/java/org/springframework/security/authorization/method/AuthorizationInterceptorsOrder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public enum AuthorizationInterceptorsOrder {
5050
*/
5151
POST_FILTER,
5252

53+
SECURE_RESULT,
54+
5355
LAST(Integer.MAX_VALUE);
5456

5557
private static final int INTERVAL = 100;

core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerAfterMethodInterceptor.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@
2525
import org.apache.commons.logging.LogFactory;
2626

2727
import org.springframework.aop.Pointcut;
28-
import org.springframework.aop.PointcutAdvisor;
29-
import org.springframework.aop.framework.AopInfrastructureBean;
30-
import org.springframework.core.Ordered;
3128
import org.springframework.core.log.LogMessage;
3229
import org.springframework.security.access.AccessDeniedException;
3330
import org.springframework.security.access.prepost.PostAuthorize;
@@ -48,8 +45,7 @@
4845
* @author Josh Cummings
4946
* @since 5.6
5047
*/
51-
public final class AuthorizationManagerAfterMethodInterceptor
52-
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
48+
public final class AuthorizationManagerAfterMethodInterceptor implements AuthorizationAdvisor {
5349

5450
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
5551

core/src/main/java/org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@
2828
import org.apache.commons.logging.LogFactory;
2929

3030
import org.springframework.aop.Pointcut;
31-
import org.springframework.aop.PointcutAdvisor;
32-
import org.springframework.aop.framework.AopInfrastructureBean;
33-
import org.springframework.core.Ordered;
3431
import org.springframework.core.log.LogMessage;
3532
import org.springframework.security.access.AccessDeniedException;
3633
import org.springframework.security.access.annotation.Secured;
@@ -52,8 +49,7 @@
5249
* @author Josh Cummings
5350
* @since 5.6
5451
*/
55-
public final class AuthorizationManagerBeforeMethodInterceptor
56-
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
52+
public final class AuthorizationManagerBeforeMethodInterceptor implements AuthorizationAdvisor {
5753

5854
private Supplier<SecurityContextHolderStrategy> securityContextHolderStrategy = SecurityContextHolder::getContextHolderStrategy;
5955

0 commit comments

Comments
 (0)