Skip to content

Commit 09ba539

Browse files
committed
Add Support for Authorizing Spring MVC Return Types
Closes gh-16059
1 parent 6438603 commit 09ba539

File tree

5 files changed

+208
-9
lines changed

5 files changed

+208
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2002-2025 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.Map;
20+
21+
import org.springframework.beans.factory.config.BeanDefinition;
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.context.annotation.Role;
25+
import org.springframework.http.HttpEntity;
26+
import org.springframework.http.ResponseEntity;
27+
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
28+
import org.springframework.web.servlet.ModelAndView;
29+
import org.springframework.web.servlet.View;
30+
31+
@Configuration
32+
class AuthorizationProxyWebConfiguration {
33+
34+
@Bean
35+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
36+
AuthorizationAdvisorProxyFactory.TargetVisitor webTargetVisitor() {
37+
return new WebTargetVisitor();
38+
}
39+
40+
static class WebTargetVisitor implements AuthorizationAdvisorProxyFactory.TargetVisitor {
41+
42+
@Override
43+
public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target) {
44+
if (target instanceof ResponseEntity<?> entity) {
45+
return new ResponseEntity<>(proxyFactory.proxy(entity.getBody()), entity.getHeaders(),
46+
entity.getStatusCode());
47+
}
48+
if (target instanceof HttpEntity<?> entity) {
49+
return new HttpEntity<>(proxyFactory.proxy(entity.getBody()), entity.getHeaders());
50+
}
51+
if (target instanceof ModelAndView mav) {
52+
View view = mav.getView();
53+
String viewName = mav.getViewName();
54+
Map<String, Object> model = (Map<String, Object>) proxyFactory.proxy(mav.getModel());
55+
ModelAndView proxied = (view != null) ? new ModelAndView(view, model)
56+
: new ModelAndView(viewName, model);
57+
proxied.setStatus(mav.getStatus());
58+
return proxied;
59+
}
60+
return null;
61+
}
62+
63+
}
64+
65+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,6 +41,9 @@ final class MethodSecuritySelector implements ImportSelector {
4141
private static final boolean isDataPresent = ClassUtils
4242
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
4343

44+
private static final boolean isWebPresent = ClassUtils
45+
.isPresent("org.springframework.web.servlet.DispatcherServlet", null);
46+
4447
private static final boolean isObservabilityPresent = ClassUtils
4548
.isPresent("io.micrometer.observation.ObservationRegistry", null);
4649

@@ -67,6 +70,9 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
6770
if (isDataPresent) {
6871
imports.add(AuthorizationProxyDataConfiguration.class.getName());
6972
}
73+
if (isWebPresent) {
74+
imports.add(AuthorizationProxyWebConfiguration.class.getName());
75+
}
7076
if (isObservabilityPresent) {
7177
imports.add(MethodObservationConfiguration.class.getName());
7278
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,9 @@ class ReactiveMethodSecuritySelector implements ImportSelector {
3838
private static final boolean isDataPresent = ClassUtils
3939
.isPresent("org.springframework.security.data.aot.hint.AuthorizeReturnObjectDataHintsRegistrar", null);
4040

41+
private static final boolean isWebPresent = ClassUtils.isPresent("org.springframework.web.server.ServerWebExchange",
42+
null);
43+
4144
private static final boolean isObservabilityPresent = ClassUtils
4245
.isPresent("io.micrometer.observation.ObservationRegistry", null);
4346

@@ -61,6 +64,9 @@ public String[] selectImports(AnnotationMetadata importMetadata) {
6164
if (isDataPresent) {
6265
imports.add(AuthorizationProxyDataConfiguration.class.getName());
6366
}
67+
if (isWebPresent) {
68+
imports.add(AuthorizationProxyWebConfiguration.class.getName());
69+
}
6470
if (isObservabilityPresent) {
6571
imports.add(ReactiveMethodObservationConfiguration.class.getName());
6672
}

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

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
import org.springframework.context.event.EventListener;
6161
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
6262
import org.springframework.core.annotation.AnnotationConfigurationException;
63+
import org.springframework.http.HttpStatusCode;
64+
import org.springframework.http.ResponseEntity;
6365
import org.springframework.security.access.AccessDeniedException;
6466
import org.springframework.security.access.PermissionEvaluator;
6567
import org.springframework.security.access.annotation.BusinessService;
@@ -90,7 +92,6 @@
9092
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
9193
import org.springframework.security.authorization.method.MethodInvocationResult;
9294
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
93-
import org.springframework.security.config.Customizer;
9495
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
9596
import org.springframework.security.config.core.GrantedAuthorityDefaults;
9697
import org.springframework.security.config.observation.SecurityObservationSettings;
@@ -109,6 +110,7 @@
109110
import org.springframework.test.context.junit.jupiter.SpringExtension;
110111
import org.springframework.web.context.ConfigurableWebApplicationContext;
111112
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
113+
import org.springframework.web.servlet.ModelAndView;
112114

113115
import static org.assertj.core.api.Assertions.assertThat;
114116
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -729,6 +731,49 @@ public void findByIdWhenUnauthorizedResultThenDenies() {
729731
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
730732
}
731733

734+
@Test
735+
@WithMockUser(authorities = "airplane:read")
736+
public void findByIdWhenAuthorizedResponseEntityThenAuthorizes() {
737+
this.spring.register(AuthorizeResultConfig.class).autowire();
738+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
739+
Flight flight = flights.webFindById("1").getBody();
740+
assertThatNoException().isThrownBy(flight::getAltitude);
741+
assertThatNoException().isThrownBy(flight::getSeats);
742+
assertThat(flights.webFindById("5").getBody()).isNull();
743+
}
744+
745+
@Test
746+
@WithMockUser(authorities = "seating:read")
747+
public void findByIdWhenUnauthorizedResponseEntityThenDenies() {
748+
this.spring.register(AuthorizeResultConfig.class).autowire();
749+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
750+
Flight flight = flights.webFindById("1").getBody();
751+
assertThatNoException().isThrownBy(flight::getSeats);
752+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
753+
}
754+
755+
@Test
756+
@WithMockUser(authorities = "airplane:read")
757+
public void findByIdWhenAuthorizedModelAndViewThenAuthorizes() {
758+
this.spring.register(AuthorizeResultConfig.class).autowire();
759+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
760+
Flight flight = (Flight) flights.webViewFindById("1").getModel().get("flight");
761+
assertThatNoException().isThrownBy(flight::getAltitude);
762+
assertThatNoException().isThrownBy(flight::getSeats);
763+
assertThat(flights.webViewFindById("5").getModel().get("flight")).isNull();
764+
}
765+
766+
@Test
767+
@WithMockUser(authorities = "seating:read")
768+
public void findByIdWhenUnauthorizedModelAndViewThenDenies() {
769+
this.spring.register(AuthorizeResultConfig.class).autowire();
770+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
771+
Flight flight = (Flight) flights.webViewFindById("1").getModel().get("flight");
772+
assertThatNoException().isThrownBy(flight::getSeats);
773+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(flight::getAltitude);
774+
assertThat(flights.webViewFindById("5").getModel().get("flight")).isNull();
775+
}
776+
732777
@Test
733778
@WithMockUser(authorities = "seating:read")
734779
public void findAllWhenUnauthorizedResultThenDenies() {
@@ -1601,8 +1646,8 @@ static class AuthorizeResultConfig {
16011646

16021647
@Bean
16031648
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
1604-
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
1605-
return (f) -> f.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
1649+
static TargetVisitor skipValueTypes() {
1650+
return TargetVisitor.defaultsSkipValueTypes();
16061651
}
16071652

16081653
@Bean
@@ -1646,6 +1691,22 @@ void remove(String id) {
16461691
this.flights.remove(id);
16471692
}
16481693

1694+
ResponseEntity<Flight> webFindById(String id) {
1695+
Flight flight = this.flights.get(id);
1696+
if (flight == null) {
1697+
return ResponseEntity.notFound().build();
1698+
}
1699+
return ResponseEntity.ok(flight);
1700+
}
1701+
1702+
ModelAndView webViewFindById(String id) {
1703+
Flight flight = this.flights.get(id);
1704+
if (flight == null) {
1705+
return new ModelAndView("error", HttpStatusCode.valueOf(404));
1706+
}
1707+
return new ModelAndView("flights", Map.of("flight", flight));
1708+
}
1709+
16491710
}
16501711

16511712
@AuthorizeReturnObject

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

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,6 +40,8 @@
4040
import org.springframework.context.annotation.Bean;
4141
import org.springframework.context.annotation.Configuration;
4242
import org.springframework.context.annotation.Role;
43+
import org.springframework.http.HttpStatusCode;
44+
import org.springframework.http.ResponseEntity;
4345
import org.springframework.security.access.AccessDeniedException;
4446
import org.springframework.security.access.PermissionEvaluator;
4547
import org.springframework.security.access.annotation.Secured;
@@ -54,9 +56,9 @@
5456
import org.springframework.security.authorization.AuthorizationDeniedException;
5557
import org.springframework.security.authorization.method.AuthorizationAdvisor;
5658
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
59+
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
5760
import org.springframework.security.authorization.method.AuthorizeReturnObject;
5861
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
59-
import org.springframework.security.config.Customizer;
6062
import org.springframework.security.config.test.SpringTestContext;
6163
import org.springframework.security.config.test.SpringTestContextExtension;
6264
import org.springframework.security.core.Authentication;
@@ -65,6 +67,7 @@
6567
import org.springframework.security.test.context.support.WithMockUser;
6668
import org.springframework.stereotype.Component;
6769
import org.springframework.test.context.junit.jupiter.SpringExtension;
70+
import org.springframework.web.servlet.ModelAndView;
6871

6972
import static org.assertj.core.api.Assertions.assertThat;
7073
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -361,6 +364,48 @@ public void findByIdWhenUnauthorizedResultThenDenies() {
361364
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
362365
}
363366

367+
@Test
368+
@WithMockUser(authorities = "airplane:read")
369+
public void findByIdWhenAuthorizedResponseEntityThenAuthorizes() {
370+
this.spring.register(AuthorizeResultConfig.class).autowire();
371+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
372+
Flight flight = flights.webFindById("1").block().getBody();
373+
assertThatNoException().isThrownBy(() -> flight.getAltitude().block());
374+
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
375+
}
376+
377+
@Test
378+
@WithMockUser(authorities = "seating:read")
379+
public void findByIdWhenUnauthorizedResponseEntityThenDenies() {
380+
this.spring.register(AuthorizeResultConfig.class).autowire();
381+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
382+
Flight flight = flights.webFindById("1").block().getBody();
383+
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
384+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
385+
}
386+
387+
@Test
388+
@WithMockUser(authorities = "airplane:read")
389+
public void findByIdWhenAuthorizedModelAndViewThenAuthorizes() {
390+
this.spring.register(AuthorizeResultConfig.class).autowire();
391+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
392+
Flight flight = (Flight) flights.webViewFindById("1").block().getModel().get("flight");
393+
assertThatNoException().isThrownBy(() -> flight.getAltitude().block());
394+
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
395+
assertThat(flights.webViewFindById("5").block().getModel().get("flight")).isNull();
396+
}
397+
398+
@Test
399+
@WithMockUser(authorities = "seating:read")
400+
public void findByIdWhenUnauthorizedModelAndViewThenDenies() {
401+
this.spring.register(AuthorizeResultConfig.class).autowire();
402+
FlightRepository flights = this.spring.getContext().getBean(FlightRepository.class);
403+
Flight flight = (Flight) flights.webViewFindById("1").block().getModel().get("flight");
404+
assertThatNoException().isThrownBy(() -> flight.getSeats().block());
405+
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> flight.getAltitude().block());
406+
assertThat(flights.webViewFindById("5").block().getModel().get("flight")).isNull();
407+
}
408+
364409
@Test
365410
@WithMockUser(authorities = "seating:read")
366411
public void findAllWhenUnauthorizedResultThenDenies() {
@@ -659,8 +704,8 @@ public static class AuthorizeResultConfig {
659704

660705
@Bean
661706
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
662-
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
663-
return (f) -> f.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.defaultsSkipValueTypes());
707+
static TargetVisitor skipValueTypes() {
708+
return TargetVisitor.defaultsSkipValueTypes();
664709
}
665710

666711
@Bean
@@ -724,6 +769,22 @@ Mono<Void> remove(String id) {
724769
return Mono.empty();
725770
}
726771

772+
Mono<ResponseEntity<Flight>> webFindById(String id) {
773+
Flight flight = this.flights.get(id);
774+
if (flight == null) {
775+
return Mono.just(ResponseEntity.notFound().build());
776+
}
777+
return Mono.just(ResponseEntity.ok(flight));
778+
}
779+
780+
Mono<ModelAndView> webViewFindById(String id) {
781+
Flight flight = this.flights.get(id);
782+
if (flight == null) {
783+
return Mono.just(new ModelAndView("error", HttpStatusCode.valueOf(404)));
784+
}
785+
return Mono.just(new ModelAndView("flights", Map.of("flight", flight)));
786+
}
787+
727788
}
728789

729790
@AuthorizeReturnObject

0 commit comments

Comments
 (0)