Skip to content

Commit 28afb4e

Browse files
jzheauxrwinch
authored andcommitted
Access Denied Handling Defaults
This introduces the capability for users to wire denial handling by request matcher, similar to how users can already do with authentication entry points. This is handy for when denial behavior differs based on the contents of the request, for example, when the Authorization header indicates an OAuth2 Bearer Token request vs Basic authentication. Fixes: gh-5478
1 parent b7ccb63 commit 28afb4e

File tree

4 files changed

+356
-9
lines changed

4 files changed

+356
-9
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java

+58-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -23,6 +23,7 @@
2323
import org.springframework.security.web.access.AccessDeniedHandler;
2424
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
2525
import org.springframework.security.web.access.ExceptionTranslationFilter;
26+
import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler;
2627
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
2728
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
2829
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
@@ -70,6 +71,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
7071

7172
private LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap<>();
7273

74+
private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
75+
7376
/**
7477
* Creates a new instance
7578
* @see HttpSecurity#exceptionHandling()
@@ -104,6 +107,26 @@ public ExceptionHandlingConfigurer<H> accessDeniedHandler(
104107
return this;
105108
}
106109

110+
/**
111+
* Sets a default {@link AccessDeniedHandler} to be used which prefers being
112+
* invoked for the provided {@link RequestMatcher}. If only a single default
113+
* {@link AccessDeniedHandler} is specified, it will be what is used for the
114+
* default {@link AccessDeniedHandler}. If multiple default
115+
* {@link AccessDeniedHandler} instances are configured, then a
116+
* {@link RequestMatcherDelegatingAccessDeniedHandler} will be used.
117+
*
118+
* @param deniedHandler the {@link AccessDeniedHandler} to use
119+
* @param preferredMatcher the {@link RequestMatcher} for this default
120+
* {@link AccessDeniedHandler}
121+
* @return the {@link ExceptionHandlingConfigurer} for further customizations
122+
* @since 5.1
123+
*/
124+
public ExceptionHandlingConfigurer<H> defaultAccessDeniedHandlerFor(
125+
AccessDeniedHandler deniedHandler, RequestMatcher preferredMatcher) {
126+
this.defaultDeniedHandlerMappings.put(preferredMatcher, deniedHandler);
127+
return this;
128+
}
129+
107130
/**
108131
* Sets the {@link AuthenticationEntryPoint} to be used.
109132
*
@@ -169,13 +192,27 @@ public void configure(H http) throws Exception {
169192
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
170193
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
171194
entryPoint, getRequestCache(http));
172-
if (accessDeniedHandler != null) {
173-
exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler);
174-
}
195+
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
196+
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
175197
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
176198
http.addFilter(exceptionTranslationFilter);
177199
}
178200

201+
/**
202+
* Gets the {@link AccessDeniedHandler} according to the rules specified by
203+
* {@link #accessDeniedHandler(AccessDeniedHandler)}
204+
* @param http the {@link HttpSecurity} used to look up shared
205+
* {@link AccessDeniedHandler}
206+
* @return the {@link AccessDeniedHandler} to use
207+
*/
208+
AccessDeniedHandler getAccessDeniedHandler(H http) {
209+
AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
210+
if (deniedHandler == null) {
211+
deniedHandler = createDefaultDeniedHandler(http);
212+
}
213+
return deniedHandler;
214+
}
215+
179216
/**
180217
* Gets the {@link AuthenticationEntryPoint} according to the rules specified by
181218
* {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
@@ -191,16 +228,28 @@ AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
191228
return entryPoint;
192229
}
193230

231+
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
232+
if (this.defaultDeniedHandlerMappings.isEmpty()) {
233+
return new AccessDeniedHandlerImpl();
234+
}
235+
if (this.defaultDeniedHandlerMappings.size() == 1) {
236+
return this.defaultDeniedHandlerMappings.values().iterator().next();
237+
}
238+
return new RequestMatcherDelegatingAccessDeniedHandler(
239+
this.defaultDeniedHandlerMappings,
240+
new AccessDeniedHandlerImpl());
241+
}
242+
194243
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
195-
if (defaultEntryPointMappings.isEmpty()) {
244+
if (this.defaultEntryPointMappings.isEmpty()) {
196245
return new Http403ForbiddenEntryPoint();
197246
}
198-
if (defaultEntryPointMappings.size() == 1) {
199-
return defaultEntryPointMappings.values().iterator().next();
247+
if (this.defaultEntryPointMappings.size() == 1) {
248+
return this.defaultEntryPointMappings.values().iterator().next();
200249
}
201250
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
202-
defaultEntryPointMappings);
203-
entryPoint.setDefaultEntryPoint(defaultEntryPointMappings.values().iterator()
251+
this.defaultEntryPointMappings);
252+
entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator()
204253
.next());
205254
return entryPoint;
206255
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2002-2018 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+
* http://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+
package org.springframework.security.config.annotation.web.configurers;
17+
18+
import org.junit.Rule;
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.http.HttpStatus;
24+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
25+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
26+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
27+
import org.springframework.security.config.test.SpringTestRule;
28+
import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners;
29+
import org.springframework.security.test.context.support.WithMockUser;
30+
import org.springframework.security.web.access.AccessDeniedHandler;
31+
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
32+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
33+
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
34+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
35+
import org.springframework.test.web.servlet.MockMvc;
36+
37+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
38+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
39+
40+
/**
41+
* @author Josh Cummings
42+
*/
43+
@RunWith(SpringJUnit4ClassRunner.class)
44+
@SecurityTestExecutionListeners
45+
public class ExceptionHandlingConfigurerAccessDeniedHandlerTests {
46+
@Autowired
47+
MockMvc mvc;
48+
49+
@Rule
50+
public final SpringTestRule spring = new SpringTestRule();
51+
52+
@Test
53+
@WithMockUser(roles = "ANYTHING")
54+
public void getWhenAccessDeniedOverriddenThenCustomizesResponseByRequest()
55+
throws Exception {
56+
this.spring.register(RequestMatcherBasedAccessDeniedHandlerConfig.class).autowire();
57+
58+
this.mvc.perform(get("/hello"))
59+
.andExpect(status().isIAmATeapot());
60+
61+
this.mvc.perform(get("/goodbye"))
62+
.andExpect(status().isForbidden());
63+
}
64+
65+
@EnableWebSecurity
66+
static class RequestMatcherBasedAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter {
67+
AccessDeniedHandler teapotDeniedHandler =
68+
(request, response, exception) ->
69+
response.setStatus(HttpStatus.I_AM_A_TEAPOT.value());
70+
71+
@Override
72+
protected void configure(HttpSecurity http) throws Exception {
73+
// @formatter:off
74+
http
75+
.authorizeRequests()
76+
.anyRequest().denyAll()
77+
.and()
78+
.exceptionHandling()
79+
.defaultAccessDeniedHandlerFor(
80+
this.teapotDeniedHandler,
81+
new AntPathRequestMatcher("/hello/**"))
82+
.defaultAccessDeniedHandlerFor(
83+
new AccessDeniedHandlerImpl(),
84+
AnyRequestMatcher.INSTANCE);
85+
// @formatter:on
86+
}
87+
}
88+
89+
@Test
90+
@WithMockUser(roles = "ANYTHING")
91+
public void getWhenAccessDeniedOverriddenByOnlyOneHandlerThenAllRequestsUseThatHandler()
92+
throws Exception {
93+
this.spring.register(SingleRequestMatcherAccessDeniedHandlerConfig.class).autowire();
94+
95+
this.mvc.perform(get("/hello"))
96+
.andExpect(status().isIAmATeapot());
97+
98+
this.mvc.perform(get("/goodbye"))
99+
.andExpect(status().isIAmATeapot());
100+
}
101+
102+
@EnableWebSecurity
103+
static class SingleRequestMatcherAccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter {
104+
AccessDeniedHandler teapotDeniedHandler =
105+
(request, response, exception) ->
106+
response.setStatus(HttpStatus.I_AM_A_TEAPOT.value());
107+
108+
@Override
109+
protected void configure(HttpSecurity http) throws Exception {
110+
// @formatter:off
111+
http
112+
.authorizeRequests()
113+
.anyRequest().denyAll()
114+
.and()
115+
.exceptionHandling()
116+
.defaultAccessDeniedHandlerFor(
117+
this.teapotDeniedHandler,
118+
new AntPathRequestMatcher("/hello/**"));
119+
// @formatter:on
120+
}
121+
}
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2002-2018 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+
* http://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+
package org.springframework.security.web.access;
17+
18+
import java.io.IOException;
19+
import java.util.LinkedHashMap;
20+
import java.util.Map.Entry;
21+
import javax.servlet.ServletException;
22+
import javax.servlet.http.HttpServletRequest;
23+
import javax.servlet.http.HttpServletResponse;
24+
25+
import org.springframework.security.access.AccessDeniedException;
26+
import org.springframework.security.web.util.matcher.RequestMatcher;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* An {@link AccessDeniedHandler} that delegates to other {@link AccessDeniedHandler}
31+
* instances based upon the type of {@link HttpServletRequest} passed into
32+
* {@link #handle(HttpServletRequest, HttpServletResponse, AccessDeniedException)}.
33+
*
34+
* @author Josh Cummings
35+
* @since 5.1
36+
*
37+
*/
38+
public final class RequestMatcherDelegatingAccessDeniedHandler implements AccessDeniedHandler {
39+
private final LinkedHashMap<RequestMatcher, AccessDeniedHandler> handlers;
40+
41+
private final AccessDeniedHandler defaultHandler;
42+
43+
/**
44+
* Creates a new instance
45+
*
46+
* @param handlers a map of {@link RequestMatcher}s to
47+
* {@link AccessDeniedHandler}s that should be used. Each is considered in the order
48+
* they are specified and only the first {@link AccessDeniedHandler} is used.
49+
* @param defaultHandler the default {@link AccessDeniedHandler} that should be used
50+
* if none of the matchers match.
51+
*/
52+
public RequestMatcherDelegatingAccessDeniedHandler(
53+
LinkedHashMap<RequestMatcher, AccessDeniedHandler> handlers,
54+
AccessDeniedHandler defaultHandler) {
55+
Assert.notEmpty(handlers, "handlers cannot be null or empty");
56+
Assert.notNull(defaultHandler, "defaultHandler cannot be null");
57+
this.handlers = new LinkedHashMap<>(handlers);
58+
this.defaultHandler = defaultHandler;
59+
}
60+
61+
public void handle(HttpServletRequest request, HttpServletResponse response,
62+
AccessDeniedException accessDeniedException) throws IOException,
63+
ServletException {
64+
for (Entry<RequestMatcher, AccessDeniedHandler> entry : this.handlers
65+
.entrySet()) {
66+
RequestMatcher matcher = entry.getKey();
67+
if (matcher.matches(request)) {
68+
AccessDeniedHandler handler = entry.getValue();
69+
handler.handle(request, response, accessDeniedException);
70+
return;
71+
}
72+
}
73+
defaultHandler.handle(request, response, accessDeniedException);
74+
}
75+
76+
}

0 commit comments

Comments
 (0)