diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java index 43da23e1f6..e8142d7551 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java @@ -78,11 +78,14 @@ public class EndpointAutoConfiguration { @Autowired private InfoPropertiesConfiguration properties; + @Autowired(required = false) + private ManagementServerProperties management; + @Autowired(required = false) private HealthAggregator healthAggregator = new OrderedHealthAggregator(); @Autowired(required = false) - Map healthIndicators = new HashMap(); + private Map healthIndicators = new HashMap(); @Autowired(required = false) private Collection publicMetrics; @@ -102,7 +105,14 @@ public class EndpointAutoConfiguration { @Bean @ConditionalOnMissingBean public HealthEndpoint healthEndpoint() { - return new HealthEndpoint(this.healthAggregator, this.healthIndicators); + // The default sensitivity depends on whether all the endpoints by default are + // secure or not. User can always override with endpoints.health.sensitive. + boolean secure = this.management != null && this.management.getSecurity() != null + && this.management.getSecurity().isEnabled(); + HealthEndpoint endpoint = new HealthEndpoint(this.healthAggregator, + this.healthIndicators); + endpoint.setSensitive(secure); + return endpoint; } @Bean diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index 0800762569..6491fae7a9 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -69,7 +69,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.event.ContextClosedEvent; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; -import org.springframework.util.ClassUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.servlet.DispatcherServlet; @@ -165,11 +164,6 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, @ConditionalOnProperty(prefix = "endpoints.health", name = "enabled", matchIfMissing = true) public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) { HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(delegate); - boolean secure = this.managementServerProperties.getSecurity() != null - && this.managementServerProperties.getSecurity().isEnabled() - && ClassUtils.isPresent( - "org.springframework.security.core.Authentication", null); - delegate.setSensitive(secure); if (this.healthMvcEndpointProperties.getMapping() != null) { healthMvcEndpoint.addStatusMapping(this.healthMvcEndpointProperties .getMapping()); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java index 6ae24f8785..16d42130c1 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java @@ -23,12 +23,15 @@ import java.util.Set; import javax.servlet.Filter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.ManagementSecurityAutoConfiguration.ManagementWebSecurityConfigurerAdapter; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; import org.springframework.boot.actuate.endpoint.mvc.ManagementErrorEndpoint; @@ -62,6 +65,9 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl @Configuration public class EndpointWebMvcChildContextConfiguration { + private static Log logger = LogFactory + .getLog(EndpointWebMvcChildContextConfiguration.class); + @Value("${error.path:/error}") private String errorPath = "/error"; @@ -135,6 +141,7 @@ public class EndpointWebMvcChildContextConfiguration { EndpointHandlerMapping mapping = new EndpointHandlerMapping(set); // In a child context we definitely want to see the parent endpoints mapping.setDetectHandlerMethodsInAncestorContexts(true); + injectIntoSecurityFilter(beanFactory, mapping); if (this.mappingCustomizers != null) { for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) { customizer.customize(mapping); @@ -143,6 +150,23 @@ public class EndpointWebMvcChildContextConfiguration { return mapping; } + private void injectIntoSecurityFilter(ListableBeanFactory beanFactory, + EndpointHandlerMapping mapping) { + // The parent context has the security filter, so we need to get it injected with + // our EndpointHandlerMapping if we can. + if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, + ManagementWebSecurityConfigurerAdapter.class).length == 1) { + ManagementWebSecurityConfigurerAdapter bean = beanFactory + .getBean(ManagementWebSecurityConfigurerAdapter.class); + bean.setEndpointHandlerMapping(mapping); + } + else { + logger.warn("No single bean of type " + + ManagementWebSecurityConfigurerAdapter.class.getSimpleName() + + " found (this might make some endpoints inaccessible without authentication)"); + } + } + /* * The error controller is present but not mapped as an endpoint in this context * because of the DispatcherServlet having had it's HandlerMapping explicitly diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java index 4273ec5167..8c156ffa04 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java @@ -22,10 +22,10 @@ import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.mvc.AnonymouslyAccessibleMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -59,8 +59,10 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity.I import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.StringUtils; /** @@ -215,6 +217,11 @@ public class ManagementSecurityAutoConfiguration { @Autowired(required = false) private EndpointHandlerMapping endpointHandlerMapping; + public void setEndpointHandlerMapping( + EndpointHandlerMapping endpointHandlerMapping) { + this.endpointHandlerMapping = endpointHandlerMapping; + } + @Override protected void configure(HttpSecurity http) throws Exception { @@ -230,8 +237,15 @@ public class ManagementSecurityAutoConfiguration { http.requestMatchers().antMatchers(paths); String[] endpointPaths = this.server.getPathsArray(getEndpointPaths( this.endpointHandlerMapping, false)); - http.authorizeRequests().antMatchers(endpointPaths).access("permitAll()") - .anyRequest().hasRole(this.management.getSecurity().getRole()); + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests = http + .authorizeRequests(); + authorizeRequests.antMatchers(endpointPaths).permitAll(); + if (this.endpointHandlerMapping != null) { + authorizeRequests.requestMatchers( + new PrincipalHandlerRequestMatcher()).permitAll(); + } + authorizeRequests.anyRequest().hasRole( + this.management.getSecurity().getRole()); http.httpBasic(); // No cookies for management endpoints by default @@ -252,6 +266,14 @@ public class ManagementSecurityAutoConfiguration { return entryPoint; } + private final class PrincipalHandlerRequestMatcher implements RequestMatcher { + @Override + public boolean matches(HttpServletRequest request) { + return ManagementWebSecurityConfigurerAdapter.this.endpointHandlerMapping + .isPrincipalHandler(request); + } + } + } private static String[] getEndpointPaths(EndpointHandlerMapping endpointHandlerMapping) { @@ -269,8 +291,7 @@ public class ManagementSecurityAutoConfiguration { Set endpoints = endpointHandlerMapping.getEndpoints(); List paths = new ArrayList(endpoints.size()); for (MvcEndpoint endpoint : endpoints) { - if (endpoint.isSensitive() == secure - || (!secure && endpoint instanceof AnonymouslyAccessibleMvcEndpoint)) { + if (endpoint.isSensitive() == secure) { String path = endpointHandlerMapping.getPath(endpoint.getPath()); paths.add(path); // Add Spring MVC-generated additional paths diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AnonymouslyAccessibleMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AnonymouslyAccessibleMvcEndpoint.java deleted file mode 100644 index a3223debbf..0000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AnonymouslyAccessibleMvcEndpoint.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2014 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 org.springframework.boot.actuate.endpoint.mvc; - -/** - * An {@link MvcEndpoint} that should be accessible without authentication - * - * @author Andy Wilkinson - * @since 1.2.0 - */ -public interface AnonymouslyAccessibleMvcEndpoint extends MvcEndpoint { - -} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java index c9854a82b6..3ef70fe38d 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java @@ -17,15 +17,20 @@ package org.springframework.boot.actuate.endpoint.mvc; import java.lang.reflect.Method; +import java.security.Principal; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import javax.servlet.http.HttpServletRequest; + import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; @@ -56,6 +61,8 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping impleme private boolean disabled = false; + private Set principalHandlers = new HashSet(); + /** * Create a new {@link EndpointHandlerMapping} instance. All {@link Endpoint}s will be * detected from the {@link ApplicationContext}. @@ -127,9 +134,33 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping impleme mapping.getHeadersCondition(), mapping.getConsumesCondition(), mapping.getProducesCondition(), mapping.getCustomCondition()); + if (handlesPrincipal(method)) { + this.principalHandlers.add(new HandlerMethod(handler, method)); + } + super.registerHandlerMethod(handler, method, modified); } + public boolean isPrincipalHandler(HttpServletRequest request) { + HandlerExecutionChain handler; + try { + handler = getHandler(request); + } + catch (Exception e) { + return false; + } + return (handler != null && this.principalHandlers.contains(handler.getHandler())); + } + + private boolean handlesPrincipal(Method method) { + for (Class type : method.getParameterTypes()) { + if (Principal.class.equals(type)) { + return true; + } + } + return false; + } + /** * @param prefix the prefix to set */ diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java index 7b77c7b7d4..561a33bed8 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java @@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Andy Wilkinson * @since 1.1.0 */ -public class HealthMvcEndpoint implements AnonymouslyAccessibleMvcEndpoint { +public class HealthMvcEndpoint implements MvcEndpoint { private Map statusMapping = new HashMap(); diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java index e4ea62572d..58eb4eae46 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/EndpointsPropertiesSampleActuatorApplicationTests.java @@ -69,5 +69,8 @@ public class EndpointsPropertiesSampleActuatorApplicationTests { assertEquals(HttpStatus.OK, entity.getStatusCode()); assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("\"status\":\"UP\"")); + System.err.println(entity.getBody()); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains("\"hello\":\"world\"")); } } diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties b/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties index cbb7f9fc5f..5777970602 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/resources/application-endpoints.properties @@ -1,2 +1,3 @@ error.path: /oops -management.contextPath: /admin \ No newline at end of file +management.contextPath: /admin +endpoints.health.sensitive: false \ No newline at end of file