Skip to content

Commit 2c049f2

Browse files
committed
Merge branch '2.6.x' into 2.7.x
Closes gh-29501
2 parents 7f021b4 + 3460c24 commit 2c049f2

File tree

11 files changed

+363
-89
lines changed

11 files changed

+363
-89
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilter.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 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.
@@ -35,6 +35,7 @@
3535
import org.springframework.security.core.Authentication;
3636
import org.springframework.security.core.context.SecurityContextHolder;
3737
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
38+
import org.springframework.web.util.UrlPathHelper;
3839

3940
/**
4041
* {@link Filter} that intercepts error dispatches to ensure authorized access to the
@@ -48,12 +49,15 @@ public class ErrorPageSecurityFilter implements Filter {
4849

4950
private static final WebInvocationPrivilegeEvaluator ALWAYS = new AlwaysAllowWebInvocationPrivilegeEvaluator();
5051

52+
private final UrlPathHelper urlPathHelper = new UrlPathHelper();
53+
5154
private final ApplicationContext context;
5255

5356
private volatile WebInvocationPrivilegeEvaluator privilegeEvaluator;
5457

5558
public ErrorPageSecurityFilter(ApplicationContext context) {
5659
this.context = context;
60+
this.urlPathHelper.setAlwaysUseFullPath(true);
5761
}
5862

5963
@Override
@@ -81,7 +85,7 @@ private boolean isAllowed(HttpServletRequest request, Integer errorCode) {
8185
if (isUnauthenticated(authentication) && isNotAuthenticationError(errorCode)) {
8286
return true;
8387
}
84-
return getPrivilegeEvaluator().isAllowed(request.getRequestURI(), authentication);
88+
return getPrivilegeEvaluator().isAllowed(this.urlPathHelper.getPathWithinApplication(request), authentication);
8589
}
8690

8791
private boolean isUnauthenticated(Authentication authentication) {

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/filter/ErrorPageSecurityFilterTests.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 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.
@@ -36,6 +36,7 @@
3636
import static org.assertj.core.api.Assertions.assertThat;
3737
import static org.mockito.ArgumentMatchers.any;
3838
import static org.mockito.ArgumentMatchers.anyString;
39+
import static org.mockito.ArgumentMatchers.eq;
3940
import static org.mockito.BDDMockito.given;
4041
import static org.mockito.BDDMockito.willThrow;
4142
import static org.mockito.Mockito.mock;
@@ -118,4 +119,30 @@ void ignorePrivilegeEvaluationForNonErrorDispatchType() throws Exception {
118119
verify(this.filterChain).doFilter(this.request, this.response);
119120
}
120121

122+
@Test
123+
void whenThereIsAContextPathAndServletIsMappedToSlashContextPathIsNotPassedToEvaluator() throws Exception {
124+
SecurityContext securityContext = mock(SecurityContext.class);
125+
SecurityContextHolder.setContext(securityContext);
126+
given(securityContext.getAuthentication()).willReturn(mock(Authentication.class));
127+
this.request.setRequestURI("/example/error");
128+
this.request.setContextPath("/example");
129+
// Servlet mapped to /
130+
this.request.setServletPath("/error");
131+
this.securityFilter.doFilter(this.request, this.response, this.filterChain);
132+
verify(this.privilegeEvaluator).isAllowed(eq("/error"), any());
133+
}
134+
135+
@Test
136+
void whenThereIsAContextPathAndServletIsMappedToWildcardPathCorrectPathIsPassedToEvaluator() throws Exception {
137+
SecurityContext securityContext = mock(SecurityContext.class);
138+
SecurityContextHolder.setContext(securityContext);
139+
given(securityContext.getAuthentication()).willReturn(mock(Authentication.class));
140+
this.request.setRequestURI("/example/dispatcher/path/error");
141+
this.request.setContextPath("/example");
142+
// Servlet mapped to /dispatcher/path/*
143+
this.request.setServletPath("/dispatcher/path");
144+
this.securityFilter.doFilter(this.request, this.response, this.filterChain);
145+
verify(this.privilegeEvaluator).isAllowed(eq("/dispatcher/path/error"), any());
146+
}
147+
121148
}

spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/AbstractErrorPageTests.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 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,12 @@ abstract class AbstractErrorPageTests {
4141
@Autowired
4242
private TestRestTemplate testRestTemplate;
4343

44+
private final String pathPrefix;
45+
46+
protected AbstractErrorPageTests(String pathPrefix) {
47+
this.pathPrefix = pathPrefix;
48+
}
49+
4450
@Test
4551
void testBadCredentials() {
4652
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "wrongpassword")
@@ -52,17 +58,17 @@ void testBadCredentials() {
5258

5359
@Test
5460
void testNoCredentials() {
55-
final ResponseEntity<JsonNode> response = this.testRestTemplate.exchange("/test", HttpMethod.GET, null,
56-
JsonNode.class);
61+
final ResponseEntity<JsonNode> response = this.testRestTemplate.exchange(this.pathPrefix + "/test",
62+
HttpMethod.GET, null, JsonNode.class);
5763
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
5864
JsonNode jsonResponse = response.getBody();
5965
assertThat(jsonResponse).isNull();
6066
}
6167

6268
@Test
6369
void testPublicNotFoundPage() {
64-
final ResponseEntity<JsonNode> response = this.testRestTemplate.exchange("/public/notfound", HttpMethod.GET,
65-
null, JsonNode.class);
70+
final ResponseEntity<JsonNode> response = this.testRestTemplate.exchange(this.pathPrefix + "/public/notfound",
71+
HttpMethod.GET, null, JsonNode.class);
6672
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
6773
JsonNode jsonResponse = response.getBody();
6874
assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found");
@@ -71,7 +77,7 @@ void testPublicNotFoundPage() {
7177
@Test
7278
void testPublicNotFoundPageWithCorrectCredentials() {
7379
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "password")
74-
.exchange("/public/notfound", HttpMethod.GET, null, JsonNode.class);
80+
.exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class);
7581
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
7682
JsonNode jsonResponse = response.getBody();
7783
assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found");
@@ -80,7 +86,7 @@ void testPublicNotFoundPageWithCorrectCredentials() {
8086
@Test
8187
void testPublicNotFoundPageWithBadCredentials() {
8288
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "wrong")
83-
.exchange("/public/notfound", HttpMethod.GET, null, JsonNode.class);
89+
.exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class);
8490
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
8591
JsonNode jsonResponse = response.getBody();
8692
assertThat(jsonResponse).isNull();
@@ -89,7 +95,7 @@ void testPublicNotFoundPageWithBadCredentials() {
8995
@Test
9096
void testCorrectCredentialsWithControllerException() {
9197
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "password")
92-
.exchange("/fail", HttpMethod.GET, null, JsonNode.class);
98+
.exchange(this.pathPrefix + "/fail", HttpMethod.GET, null, JsonNode.class);
9399
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
94100
JsonNode jsonResponse = response.getBody();
95101
assertThat(jsonResponse.get("error").asText()).isEqualTo("Internal Server Error");
@@ -98,7 +104,7 @@ void testCorrectCredentialsWithControllerException() {
98104
@Test
99105
void testCorrectCredentials() {
100106
final ResponseEntity<String> response = this.testRestTemplate.withBasicAuth("username", "password")
101-
.exchange("/test", HttpMethod.GET, null, String.class);
107+
.exchange(this.pathPrefix + "/test", HttpMethod.GET, null, String.class);
102108
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
103109
response.getBody();
104110
assertThat(response.getBody()).isEqualTo("test");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2012-2022 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 smoketest.web.secure;
18+
19+
import com.fasterxml.jackson.databind.JsonNode;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.boot.test.web.client.TestRestTemplate;
24+
import org.springframework.http.HttpMethod;
25+
import org.springframework.http.HttpStatus;
26+
import org.springframework.http.ResponseEntity;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
/**
31+
* Abstract base class for tests to ensure that the error page is accessible only to
32+
* authorized users.
33+
*
34+
* @author Madhura Bhave
35+
*/
36+
abstract class AbstractUnauthenticatedErrorPageTests {
37+
38+
@Autowired
39+
private TestRestTemplate testRestTemplate;
40+
41+
private final String pathPrefix;
42+
43+
protected AbstractUnauthenticatedErrorPageTests(String pathPrefix) {
44+
this.pathPrefix = pathPrefix;
45+
}
46+
47+
@Test
48+
void testBadCredentials() {
49+
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "wrongpassword")
50+
.exchange(this.pathPrefix + "/test", HttpMethod.GET, null, JsonNode.class);
51+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
52+
JsonNode jsonResponse = response.getBody();
53+
assertThat(jsonResponse.get("error").asText()).isEqualTo("Unauthorized");
54+
}
55+
56+
@Test
57+
void testNoCredentials() {
58+
final ResponseEntity<JsonNode> response = this.testRestTemplate.exchange(this.pathPrefix + "/test",
59+
HttpMethod.GET, null, JsonNode.class);
60+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
61+
JsonNode jsonResponse = response.getBody();
62+
assertThat(jsonResponse.get("error").asText()).isEqualTo("Unauthorized");
63+
}
64+
65+
@Test
66+
void testPublicNotFoundPage() {
67+
final ResponseEntity<JsonNode> response = this.testRestTemplate.exchange(this.pathPrefix + "/public/notfound",
68+
HttpMethod.GET, null, JsonNode.class);
69+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
70+
JsonNode jsonResponse = response.getBody();
71+
assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found");
72+
}
73+
74+
@Test
75+
void testPublicNotFoundPageWithCorrectCredentials() {
76+
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "password")
77+
.exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class);
78+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
79+
JsonNode jsonResponse = response.getBody();
80+
assertThat(jsonResponse.get("error").asText()).isEqualTo("Not Found");
81+
}
82+
83+
@Test
84+
void testPublicNotFoundPageWithBadCredentials() {
85+
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "wrong")
86+
.exchange(this.pathPrefix + "/public/notfound", HttpMethod.GET, null, JsonNode.class);
87+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
88+
JsonNode jsonResponse = response.getBody();
89+
assertThat(jsonResponse.get("error").asText()).isEqualTo("Unauthorized");
90+
}
91+
92+
@Test
93+
void testCorrectCredentialsWithControllerException() {
94+
final ResponseEntity<JsonNode> response = this.testRestTemplate.withBasicAuth("username", "password")
95+
.exchange(this.pathPrefix + "/fail", HttpMethod.GET, null, JsonNode.class);
96+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
97+
JsonNode jsonResponse = response.getBody();
98+
assertThat(jsonResponse.get("error").asText()).isEqualTo("Internal Server Error");
99+
}
100+
101+
@Test
102+
void testCorrectCredentials() {
103+
final ResponseEntity<String> response = this.testRestTemplate.withBasicAuth("username", "password")
104+
.exchange(this.pathPrefix + "/test", HttpMethod.GET, null, String.class);
105+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
106+
assertThat(response.getBody()).isEqualTo("test");
107+
}
108+
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2012-2022 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 smoketest.web.secure;
18+
19+
import org.springframework.boot.test.context.SpringBootTest;
20+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
21+
22+
/**
23+
* Tests to ensure that the error page with a custom context path is accessible only to
24+
* authorized users.
25+
*
26+
* @author Madhura Bhave
27+
*/
28+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
29+
classes = { AbstractErrorPageTests.TestConfiguration.class, ErrorPageTests.SecurityConfiguration.class,
30+
SampleWebSecureApplication.class },
31+
properties = { "server.error.include-message=always", "spring.security.user.name=username",
32+
"spring.security.user.password=password", "server.servlet.context-path=/example" })
33+
class CustomContextPathErrorPageTests extends AbstractErrorPageTests {
34+
35+
CustomContextPathErrorPageTests() {
36+
super("");
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2012-2022 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 smoketest.web.secure;
18+
19+
import org.springframework.boot.test.context.SpringBootTest;
20+
21+
/**
22+
* Tests for error page that permits access to all with a custom context path.
23+
*
24+
* @author Madhura Bhave
25+
*/
26+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
27+
classes = { AbstractErrorPageTests.TestConfiguration.class,
28+
UnauthenticatedErrorPageTests.SecurityConfiguration.class, SampleWebSecureApplication.class },
29+
properties = { "server.error.include-message=always", "spring.security.user.name=username",
30+
"spring.security.user.password=password", "server.servlet.context-path=/example" })
31+
class CustomContextPathUnauthenticatedErrorPageTests extends AbstractUnauthenticatedErrorPageTests {
32+
33+
CustomContextPathUnauthenticatedErrorPageTests() {
34+
super("");
35+
}
36+
37+
}

0 commit comments

Comments
 (0)