Skip to content

Commit c3e4754

Browse files
committed
EndpointRequest should check that the request is to the mgmt context
Fixes gh-15702
1 parent 59430a2 commit c3e4754

File tree

5 files changed

+174
-1
lines changed

5 files changed

+174
-1
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131

3232
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3333
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
34+
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
3435
import org.springframework.boot.actuate.endpoint.EndpointId;
3536
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3637
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
3738
import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher;
39+
import org.springframework.context.ApplicationContext;
3840
import org.springframework.core.annotation.AnnotatedElementUtils;
3941
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
4042
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
@@ -239,9 +241,28 @@ private List<ServerWebExchangeMatcher> getDelegateMatchers(Set<String> paths) {
239241
@Override
240242
protected Mono<MatchResult> matches(ServerWebExchange exchange,
241243
Supplier<PathMappedEndpoints> context) {
244+
if (!isManagementContext(exchange)) {
245+
return MatchResult.notMatch();
246+
}
242247
return this.delegate.matches(exchange);
243248
}
244249

250+
static boolean isManagementContext(ServerWebExchange exchange) {
251+
ApplicationContext applicationContext = exchange.getApplicationContext();
252+
if (ManagementPortType.get(applicationContext
253+
.getEnvironment()) == ManagementPortType.DIFFERENT) {
254+
if (applicationContext.getParent() == null) {
255+
return false;
256+
}
257+
String managementContextId = applicationContext.getParent().getId()
258+
+ ":management";
259+
if (!managementContextId.equals(applicationContext.getId())) {
260+
return false;
261+
}
262+
}
263+
return true;
264+
}
265+
245266
}
246267

247268
/**
@@ -273,6 +294,9 @@ private ServerWebExchangeMatcher createDelegate(
273294
@Override
274295
protected Mono<MatchResult> matches(ServerWebExchange exchange,
275296
Supplier<WebEndpointProperties> context) {
297+
if (!EndpointServerWebExchangeMatcher.isManagementContext(exchange)) {
298+
return MatchResult.notMatch();
299+
}
276300
return this.delegate.matches(exchange);
277301
}
278302

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3333
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
34+
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
3435
import org.springframework.boot.actuate.endpoint.EndpointId;
3536
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3637
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
@@ -43,6 +44,7 @@
4344
import org.springframework.util.Assert;
4445
import org.springframework.util.StringUtils;
4546
import org.springframework.web.context.WebApplicationContext;
47+
import org.springframework.web.context.support.WebApplicationContextUtils;
4648

4749
/**
4850
* Factory that can be used to create a {@link RequestMatcher} for actuator endpoint
@@ -133,6 +135,19 @@ protected final void initialized(Supplier<WebApplicationContext> context) {
133135
@Override
134136
protected final boolean matches(HttpServletRequest request,
135137
Supplier<WebApplicationContext> context) {
138+
WebApplicationContext applicationContext = WebApplicationContextUtils
139+
.getRequiredWebApplicationContext(request.getServletContext());
140+
if (ManagementPortType.get(applicationContext
141+
.getEnvironment()) == ManagementPortType.DIFFERENT) {
142+
if (applicationContext.getParent() == null) {
143+
return false;
144+
}
145+
String managementContextId = applicationContext.getParent().getId()
146+
+ ":management";
147+
if (!managementContextId.equals(applicationContext.getId())) {
148+
return false;
149+
}
150+
}
136151
return this.delegate.matches(request);
137152
}
138153

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementPortType.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@ public enum ManagementPortType {
4141
*/
4242
DIFFERENT;
4343

44-
static ManagementPortType get(Environment environment) {
44+
/**
45+
* Look at the given environment to determine if the {@link ManagementPortType} is
46+
* {@link #DISABLED}, {@link #SAME} or {@link #DIFFERENT}.
47+
* @param environment the Spring environment
48+
* @return {@link #DISABLED} if `management.server.port` is set to a negative value,
49+
* {@link #SAME} if `management.server.port` is not specified or equal to
50+
* `server.port`and {@link #DIFFERENT} otherwise.
51+
* @since 2.1.4
52+
*/
53+
public static ManagementPortType get(Environment environment) {
4554
Integer serverPort = getPortProperty(environment, "server.");
4655
Integer managementPort = getPortProperty(environment, "management.server.");
4756
if (managementPort != null && managementPort < 0) {

spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ public void testHome() {
5656
assertThat(entity.getBody()).contains("Hello World");
5757
}
5858

59+
@Test
60+
public void actuatorPathOnMainPortShouldNotMatch() {
61+
ResponseEntity<String> entity = new TestRestTemplate().getForEntity(
62+
"http://localhost:" + this.port + "/actuator/health",
63+
String.class);
64+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
65+
}
66+
5967
@Test
6068
public void testSecureActuator() {
6169
ResponseEntity<String> entity = new TestRestTemplate().getForEntity(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2012-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+
17+
package sample.secure.webflux;
18+
19+
import java.util.Base64;
20+
21+
import org.assertj.core.api.Assertions;
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
27+
import org.springframework.boot.actuate.autoconfigure.web.server.LocalManagementPort;
28+
import org.springframework.boot.actuate.web.mappings.MappingsEndpoint;
29+
import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
30+
import org.springframework.boot.test.context.SpringBootTest;
31+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
32+
import org.springframework.boot.web.server.LocalServerPort;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.security.config.web.server.ServerHttpSecurity;
36+
import org.springframework.security.web.server.SecurityWebFilterChain;
37+
import org.springframework.test.context.junit4.SpringRunner;
38+
import org.springframework.test.web.reactive.server.WebTestClient;
39+
40+
/**
41+
* Integration tests for separate management and main service ports.
42+
*
43+
* @author Madhura Bhave
44+
*/
45+
@RunWith(SpringRunner.class)
46+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
47+
"management.server.port=0" }, classes = {
48+
ManagementPortSampleSecureWebFluxTests.SecurityConfiguration.class,
49+
SampleSecureWebFluxApplication.class })
50+
public class ManagementPortSampleSecureWebFluxTests {
51+
52+
@LocalServerPort
53+
private int port = 9010;
54+
55+
@LocalManagementPort
56+
private int managementPort = 9011;
57+
58+
@Autowired
59+
private WebTestClient webClient;
60+
61+
@Test
62+
public void testHome() {
63+
this.webClient.get().uri("http://localhost:" + this.port, String.class)
64+
.header("Authorization", "basic " + getBasicAuth()).exchange()
65+
.expectStatus().isOk().expectBody(String.class).isEqualTo("Hello user");
66+
}
67+
68+
@Test
69+
public void actuatorPathOnMainPortShouldNotMatch() {
70+
this.webClient.get()
71+
.uri("http://localhost:" + this.port + "/actuator", String.class)
72+
.exchange().expectStatus().isUnauthorized();
73+
this.webClient.get()
74+
.uri("http://localhost:" + this.port + "/actuator/health", String.class)
75+
.exchange().expectStatus().isUnauthorized();
76+
}
77+
78+
@Test
79+
public void testSecureActuator() {
80+
this.webClient.get()
81+
.uri("http://localhost:" + this.managementPort + "/actuator/env",
82+
String.class)
83+
.exchange().expectStatus().isUnauthorized();
84+
}
85+
86+
@Test
87+
public void testInsecureActuator() {
88+
String responseBody = this.webClient.get()
89+
.uri("http://localhost:" + this.managementPort + "/actuator/health",
90+
String.class)
91+
.exchange().expectStatus().isOk().expectBody(String.class).returnResult()
92+
.getResponseBody();
93+
Assertions.assertThat(responseBody).contains("\"status\":\"UP\"");
94+
}
95+
96+
private String getBasicAuth() {
97+
return new String(Base64.getEncoder().encode(("user:password").getBytes()));
98+
}
99+
100+
@Configuration
101+
static class SecurityConfiguration {
102+
103+
@Bean
104+
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
105+
return http.authorizeExchange().matchers(EndpointRequest.to("health", "info"))
106+
.permitAll()
107+
.matchers(EndpointRequest.toAnyEndpoint()
108+
.excluding(MappingsEndpoint.class))
109+
.hasRole("ACTUATOR")
110+
.matchers(PathRequest.toStaticResources().atCommonLocations())
111+
.permitAll().pathMatchers("/login").permitAll().anyExchange()
112+
.authenticated().and().httpBasic().and().build();
113+
}
114+
115+
}
116+
117+
}

0 commit comments

Comments
 (0)