From c3e47541ad16a1507582470f73effdeaeee1a5aa Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 19 Feb 2019 16:34:28 -0800 Subject: [PATCH] EndpointRequest should check that the request is to the mgmt context Fixes gh-15702 --- .../security/reactive/EndpointRequest.java | 24 ++++ .../security/servlet/EndpointRequest.java | 15 +++ .../web/server/ManagementPortType.java | 11 +- ...AndPathSampleActuatorApplicationTests.java | 8 ++ ...anagementPortSampleSecureWebFluxTests.java | 117 ++++++++++++++++++ 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/ManagementPortSampleSecureWebFluxTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java index 15ed8c5e2d07..07f7331241ca 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java @@ -31,10 +31,12 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher; +import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; @@ -239,9 +241,28 @@ private List getDelegateMatchers(Set paths) { @Override protected Mono matches(ServerWebExchange exchange, Supplier context) { + if (!isManagementContext(exchange)) { + return MatchResult.notMatch(); + } return this.delegate.matches(exchange); } + static boolean isManagementContext(ServerWebExchange exchange) { + ApplicationContext applicationContext = exchange.getApplicationContext(); + if (ManagementPortType.get(applicationContext + .getEnvironment()) == ManagementPortType.DIFFERENT) { + if (applicationContext.getParent() == null) { + return false; + } + String managementContextId = applicationContext.getParent().getId() + + ":management"; + if (!managementContextId.equals(applicationContext.getId())) { + return false; + } + } + return true; + } + } /** @@ -273,6 +294,9 @@ private ServerWebExchangeMatcher createDelegate( @Override protected Mono matches(ServerWebExchange exchange, Supplier context) { + if (!EndpointServerWebExchangeMatcher.isManagementContext(exchange)) { + return MatchResult.notMatch(); + } return this.delegate.matches(exchange); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java index 92db210eaf1a..aa96aedd88c2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java @@ -31,6 +31,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; @@ -43,6 +44,7 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; /** * Factory that can be used to create a {@link RequestMatcher} for actuator endpoint @@ -133,6 +135,19 @@ protected final void initialized(Supplier context) { @Override protected final boolean matches(HttpServletRequest request, Supplier context) { + WebApplicationContext applicationContext = WebApplicationContextUtils + .getRequiredWebApplicationContext(request.getServletContext()); + if (ManagementPortType.get(applicationContext + .getEnvironment()) == ManagementPortType.DIFFERENT) { + if (applicationContext.getParent() == null) { + return false; + } + String managementContextId = applicationContext.getParent().getId() + + ":management"; + if (!managementContextId.equals(applicationContext.getId())) { + return false; + } + } return this.delegate.matches(request); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementPortType.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementPortType.java index eccc7b59e1e3..6c44189affad 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementPortType.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementPortType.java @@ -41,7 +41,16 @@ public enum ManagementPortType { */ DIFFERENT; - static ManagementPortType get(Environment environment) { + /** + * Look at the given environment to determine if the {@link ManagementPortType} is + * {@link #DISABLED}, {@link #SAME} or {@link #DIFFERENT}. + * @param environment the Spring environment + * @return {@link #DISABLED} if `management.server.port` is set to a negative value, + * {@link #SAME} if `management.server.port` is not specified or equal to + * `server.port`and {@link #DIFFERENT} otherwise. + * @since 2.1.4 + */ + public static ManagementPortType get(Environment environment) { Integer serverPort = getPortProperty(environment, "server."); Integer managementPort = getPortProperty(environment, "management.server."); if (managementPort != null && managementPort < 0) { diff --git a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java index 7313677c0830..bc7efdf6f419 100644 --- a/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator-custom-security/src/test/java/sample/actuator/customsecurity/ManagementPortAndPathSampleActuatorApplicationTests.java @@ -56,6 +56,14 @@ public void testHome() { assertThat(entity.getBody()).contains("Hello World"); } + @Test + public void actuatorPathOnMainPortShouldNotMatch() { + ResponseEntity entity = new TestRestTemplate().getForEntity( + "http://localhost:" + this.port + "/actuator/health", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + @Test public void testSecureActuator() { ResponseEntity entity = new TestRestTemplate().getForEntity( diff --git a/spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/ManagementPortSampleSecureWebFluxTests.java b/spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/ManagementPortSampleSecureWebFluxTests.java new file mode 100644 index 000000000000..18492d1bcedc --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-webflux/src/test/java/sample/secure/webflux/ManagementPortSampleSecureWebFluxTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.secure.webflux; + +import java.util.Base64; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest; +import org.springframework.boot.actuate.autoconfigure.web.server.LocalManagementPort; +import org.springframework.boot.actuate.web.mappings.MappingsEndpoint; +import org.springframework.boot.autoconfigure.security.reactive.PathRequest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests for separate management and main service ports. + * + * @author Madhura Bhave + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { + "management.server.port=0" }, classes = { + ManagementPortSampleSecureWebFluxTests.SecurityConfiguration.class, + SampleSecureWebFluxApplication.class }) +public class ManagementPortSampleSecureWebFluxTests { + + @LocalServerPort + private int port = 9010; + + @LocalManagementPort + private int managementPort = 9011; + + @Autowired + private WebTestClient webClient; + + @Test + public void testHome() { + this.webClient.get().uri("http://localhost:" + this.port, String.class) + .header("Authorization", "basic " + getBasicAuth()).exchange() + .expectStatus().isOk().expectBody(String.class).isEqualTo("Hello user"); + } + + @Test + public void actuatorPathOnMainPortShouldNotMatch() { + this.webClient.get() + .uri("http://localhost:" + this.port + "/actuator", String.class) + .exchange().expectStatus().isUnauthorized(); + this.webClient.get() + .uri("http://localhost:" + this.port + "/actuator/health", String.class) + .exchange().expectStatus().isUnauthorized(); + } + + @Test + public void testSecureActuator() { + this.webClient.get() + .uri("http://localhost:" + this.managementPort + "/actuator/env", + String.class) + .exchange().expectStatus().isUnauthorized(); + } + + @Test + public void testInsecureActuator() { + String responseBody = this.webClient.get() + .uri("http://localhost:" + this.managementPort + "/actuator/health", + String.class) + .exchange().expectStatus().isOk().expectBody(String.class).returnResult() + .getResponseBody(); + Assertions.assertThat(responseBody).contains("\"status\":\"UP\""); + } + + private String getBasicAuth() { + return new String(Base64.getEncoder().encode(("user:password").getBytes())); + } + + @Configuration + static class SecurityConfiguration { + + @Bean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + return http.authorizeExchange().matchers(EndpointRequest.to("health", "info")) + .permitAll() + .matchers(EndpointRequest.toAnyEndpoint() + .excluding(MappingsEndpoint.class)) + .hasRole("ACTUATOR") + .matchers(PathRequest.toStaticResources().atCommonLocations()) + .permitAll().pathMatchers("/login").permitAll().anyExchange() + .authenticated().and().httpBasic().and().build(); + } + + } + +}