Skip to content

Commit 6adaed4

Browse files
committed
Exceptions for Authorized Objects should propagate when returned from a Controller
Closes spring-projectsgh-16058 Signed-off-by: Evgeniy Cheban <[email protected]>
1 parent ff8b77d commit 6adaed4

File tree

2 files changed

+170
-1
lines changed

2 files changed

+170
-1
lines changed

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,41 @@
1616

1717
package org.springframework.security.config.annotation.method.configuration;
1818

19+
import java.util.List;
1920
import java.util.Map;
2021

22+
import jakarta.servlet.http.HttpServletRequest;
23+
import jakarta.servlet.http.HttpServletResponse;
24+
2125
import org.springframework.beans.factory.config.BeanDefinition;
2226
import org.springframework.context.annotation.Bean;
2327
import org.springframework.context.annotation.Configuration;
2428
import org.springframework.context.annotation.Role;
2529
import org.springframework.http.HttpEntity;
2630
import org.springframework.http.ResponseEntity;
31+
import org.springframework.http.converter.HttpMessageNotWritableException;
32+
import org.springframework.security.access.AccessDeniedException;
2733
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
34+
import org.springframework.security.web.util.ThrowableAnalyzer;
35+
import org.springframework.web.servlet.HandlerExceptionResolver;
2836
import org.springframework.web.servlet.ModelAndView;
2937
import org.springframework.web.servlet.View;
38+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
3039

3140
@Configuration
32-
class AuthorizationProxyWebConfiguration {
41+
class AuthorizationProxyWebConfiguration implements WebMvcConfigurer {
3342

3443
@Bean
3544
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
3645
AuthorizationAdvisorProxyFactory.TargetVisitor webTargetVisitor() {
3746
return new WebTargetVisitor();
3847
}
3948

49+
@Override
50+
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
51+
resolvers.add(0, new HttpMessageNotWritableAccessDeniedExceptionResolver());
52+
}
53+
4054
static class WebTargetVisitor implements AuthorizationAdvisorProxyFactory.TargetVisitor {
4155

4256
@Override
@@ -62,4 +76,27 @@ public Object visit(AuthorizationAdvisorProxyFactory proxyFactory, Object target
6276

6377
}
6478

79+
static class HttpMessageNotWritableAccessDeniedExceptionResolver implements HandlerExceptionResolver {
80+
81+
final ThrowableAnalyzer throwableAnalyzer = new ThrowableAnalyzer();
82+
83+
@Override
84+
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
85+
Exception ex) {
86+
// Only resolves AccessDeniedException if it occurred during serialization,
87+
// otherwise lets the user-defined handler deal with it.
88+
if (ex instanceof HttpMessageNotWritableException) {
89+
Throwable[] chain = this.throwableAnalyzer.determineCauseChain(ex);
90+
Throwable t = this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, chain);
91+
if (t instanceof AccessDeniedException) {
92+
return new ModelAndView((model, req, res) -> {
93+
throw ex;
94+
});
95+
}
96+
}
97+
return null;
98+
}
99+
100+
}
101+
65102
}

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.micrometer.observation.ObservationRegistry;
3434
import io.micrometer.observation.ObservationTextPublisher;
3535
import jakarta.annotation.security.DenyAll;
36+
import org.aopalliance.aop.Advice;
3637
import org.aopalliance.intercept.MethodInterceptor;
3738
import org.aopalliance.intercept.MethodInvocation;
3839
import org.junit.jupiter.api.Test;
@@ -42,6 +43,7 @@
4243
import org.mockito.Mockito;
4344

4445
import org.springframework.aop.Advisor;
46+
import org.springframework.aop.Pointcut;
4547
import org.springframework.aop.config.AopConfigUtils;
4648
import org.springframework.aop.support.DefaultPointcutAdvisor;
4749
import org.springframework.aop.support.JdkRegexpMethodPointcut;
@@ -63,6 +65,7 @@
6365
import org.springframework.core.annotation.AnnotationConfigurationException;
6466
import org.springframework.core.annotation.Order;
6567
import org.springframework.http.HttpStatusCode;
68+
import org.springframework.http.MediaType;
6669
import org.springframework.http.ResponseEntity;
6770
import org.springframework.security.access.AccessDeniedException;
6871
import org.springframework.security.access.PermissionEvaluator;
@@ -95,6 +98,7 @@
9598
import org.springframework.security.authorization.method.MethodInvocationResult;
9699
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
97100
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
101+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
98102
import org.springframework.security.config.core.GrantedAuthorityDefaults;
99103
import org.springframework.security.config.observation.SecurityObservationSettings;
100104
import org.springframework.security.config.test.SpringTestContext;
@@ -110,9 +114,15 @@
110114
import org.springframework.test.context.ContextConfiguration;
111115
import org.springframework.test.context.TestExecutionListeners;
112116
import org.springframework.test.context.junit.jupiter.SpringExtension;
117+
import org.springframework.test.web.servlet.MockMvc;
118+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
119+
import org.springframework.web.bind.annotation.GetMapping;
120+
import org.springframework.web.bind.annotation.RequestParam;
121+
import org.springframework.web.bind.annotation.RestController;
113122
import org.springframework.web.context.ConfigurableWebApplicationContext;
114123
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
115124
import org.springframework.web.servlet.ModelAndView;
125+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
116126

117127
import static org.assertj.core.api.Assertions.assertThat;
118128
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -127,6 +137,9 @@
127137
import static org.mockito.Mockito.times;
128138
import static org.mockito.Mockito.verify;
129139
import static org.mockito.Mockito.verifyNoInteractions;
140+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
141+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
142+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
130143

131144
/**
132145
* Tests for {@link PrePostMethodSecurityConfiguration}.
@@ -148,6 +161,9 @@ public class PrePostMethodSecurityConfigurationTests {
148161
@Autowired(required = false)
149162
BusinessService businessService;
150163

164+
@Autowired(required = false)
165+
MockMvc mvc;
166+
151167
@WithMockUser
152168
@Test
153169
public void customMethodSecurityPreAuthorizeAdminWhenRoleUserThenAccessDeniedException() {
@@ -1181,6 +1197,50 @@ void autowireWhenDefaultsThenAdvisorAnnotationsAreSorted() {
11811197
}
11821198
}
11831199

1200+
@Test
1201+
void getWhenPostAuthorizeAuthenticationNameMatchesThenRespondsWithOk() throws Exception {
1202+
this.spring.register(WebMvcMethodSecurityConfig.class, BasicController.class).autowire();
1203+
// @formatter:off
1204+
MockHttpServletRequestBuilder requestWithUser = get("/authorized-person")
1205+
.param("name", "rob")
1206+
.with(user("rob"));
1207+
// @formatter:on
1208+
this.mvc.perform(requestWithUser).andExpect(status().isOk());
1209+
}
1210+
1211+
@Test
1212+
void getWhenPostAuthorizeAuthenticationNameNotMatchThenRespondsWithForbidden() throws Exception {
1213+
this.spring.register(WebMvcMethodSecurityConfig.class, BasicController.class).autowire();
1214+
// @formatter:off
1215+
MockHttpServletRequestBuilder requestWithUser = get("/authorized-person")
1216+
.param("name", "john")
1217+
.with(user("rob"));
1218+
// @formatter:on
1219+
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
1220+
}
1221+
1222+
@Test
1223+
void getWhenCustomAdvisorAuthenticationNameMatchesThenRespondsWithOk() throws Exception {
1224+
this.spring.register(WebMvcMethodSecurityCustomAdvisorConfig.class, BasicController.class).autowire();
1225+
// @formatter:off
1226+
MockHttpServletRequestBuilder requestWithUser = get("/authorized-person")
1227+
.param("name", "rob")
1228+
.with(user("rob"));
1229+
// @formatter:on
1230+
this.mvc.perform(requestWithUser).andExpect(status().isOk());
1231+
}
1232+
1233+
@Test
1234+
void getWhenCustomAdvisorAuthenticationNameNotMatchThenRespondsWithForbidden() throws Exception {
1235+
this.spring.register(WebMvcMethodSecurityCustomAdvisorConfig.class, BasicController.class).autowire();
1236+
// @formatter:off
1237+
MockHttpServletRequestBuilder requestWithUser = get("/authorized-person")
1238+
.param("name", "john")
1239+
.with(user("rob"));
1240+
// @formatter:on
1241+
this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
1242+
}
1243+
11841244
private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
11851245
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
11861246
}
@@ -1919,4 +1979,76 @@ void onRequestDenied(AuthorizationDeniedEvent<? extends MethodInvocation> denied
19191979

19201980
}
19211981

1982+
@EnableWebMvc
1983+
@EnableWebSecurity
1984+
@EnableMethodSecurity
1985+
static class WebMvcMethodSecurityConfig {
1986+
1987+
}
1988+
1989+
@EnableWebMvc
1990+
@EnableWebSecurity
1991+
@EnableMethodSecurity
1992+
static class WebMvcMethodSecurityCustomAdvisorConfig {
1993+
1994+
@Bean
1995+
AuthorizationAdvisor customAdvisor(SecurityContextHolderStrategy strategy) {
1996+
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
1997+
pointcut.setPattern(".*AuthorizedPerson.*getName");
1998+
return new AuthorizationAdvisor() {
1999+
@Override
2000+
public Object invoke(MethodInvocation mi) throws Throwable {
2001+
Authentication auth = strategy.getContext().getAuthentication();
2002+
Object result = mi.proceed();
2003+
if (auth.getName().equals(result)) {
2004+
return result;
2005+
}
2006+
throw new AccessDeniedException("Access Denied for User '" + auth.getName() + "'");
2007+
}
2008+
2009+
@Override
2010+
public Pointcut getPointcut() {
2011+
return pointcut;
2012+
}
2013+
2014+
@Override
2015+
public Advice getAdvice() {
2016+
return this;
2017+
}
2018+
2019+
@Override
2020+
public int getOrder() {
2021+
return AuthorizationInterceptorsOrder.POST_FILTER.getOrder() + 1;
2022+
}
2023+
};
2024+
}
2025+
2026+
}
2027+
2028+
@RestController
2029+
static class BasicController {
2030+
2031+
@AuthorizeReturnObject
2032+
@GetMapping(value = "/authorized-person", produces = MediaType.APPLICATION_JSON_VALUE)
2033+
AuthorizedPerson getAuthorizedPerson(@RequestParam String name) {
2034+
return new AuthorizedPerson(name);
2035+
}
2036+
2037+
}
2038+
2039+
public static class AuthorizedPerson {
2040+
2041+
final String name;
2042+
2043+
AuthorizedPerson(String name) {
2044+
this.name = name;
2045+
}
2046+
2047+
@PostAuthorize("returnObject == authentication.name")
2048+
public String getName() {
2049+
return this.name;
2050+
}
2051+
2052+
}
2053+
19222054
}

0 commit comments

Comments
 (0)