Better handling of anonymously accessible endpoints

Shares the /health endpoint request mapping between security config
and MVC dispatcher. Generalizes so that instead of a marker
interface (AnonymouslyAccessibleMvcEndpoint), an MvcEndpoint
signals that it wants to control its own access rules by adding
a Principal to the @RequestMapping method parameters (more @MVC).

Fixes gh-2015 slightly differently
This commit is contained in:
Dave Syer 2014-11-28 06:33:30 +00:00
parent 2ce057ca96
commit 3c1e48c89a
9 changed files with 99 additions and 42 deletions

View File

@ -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<String, HealthIndicator> healthIndicators = new HashMap<String, HealthIndicator>();
private Map<String, HealthIndicator> healthIndicators = new HashMap<String, HealthIndicator>();
@Autowired(required = false)
private Collection<PublicMetrics> 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

View File

@ -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());

View File

@ -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

View File

@ -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<HttpSecurity>.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<? extends MvcEndpoint> endpoints = endpointHandlerMapping.getEndpoints();
List<String> paths = new ArrayList<String>(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

View File

@ -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 {
}

View File

@ -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<HandlerMethod> principalHandlers = new HashSet<HandlerMethod>();
/**
* 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
*/

View File

@ -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<String, HttpStatus> statusMapping = new HashMap<String, HttpStatus>();

View File

@ -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\""));
}
}

View File

@ -1,2 +1,3 @@
error.path: /oops
management.contextPath: /admin
management.contextPath: /admin
endpoints.health.sensitive: false