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
pull/2029/head
Dave Syer 10 years ago
parent 2ce057ca96
commit 3c1e48c89a

@ -78,11 +78,14 @@ public class EndpointAutoConfiguration {
@Autowired @Autowired
private InfoPropertiesConfiguration properties; private InfoPropertiesConfiguration properties;
@Autowired(required = false)
private ManagementServerProperties management;
@Autowired(required = false) @Autowired(required = false)
private HealthAggregator healthAggregator = new OrderedHealthAggregator(); private HealthAggregator healthAggregator = new OrderedHealthAggregator();
@Autowired(required = false) @Autowired(required = false)
Map<String, HealthIndicator> healthIndicators = new HashMap<String, HealthIndicator>(); private Map<String, HealthIndicator> healthIndicators = new HashMap<String, HealthIndicator>();
@Autowired(required = false) @Autowired(required = false)
private Collection<PublicMetrics> publicMetrics; private Collection<PublicMetrics> publicMetrics;
@ -102,7 +105,14 @@ public class EndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public HealthEndpoint healthEndpoint() { 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 @Bean

@ -69,7 +69,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
@ -165,11 +164,6 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
@ConditionalOnProperty(prefix = "endpoints.health", name = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "endpoints.health", name = "enabled", matchIfMissing = true)
public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) { public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) {
HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(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) { if (this.healthMvcEndpointProperties.getMapping() != null) {
healthMvcEndpoint.addStatusMapping(this.healthMvcEndpointProperties healthMvcEndpoint.addStatusMapping(this.healthMvcEndpointProperties
.getMapping()); .getMapping());

@ -23,12 +23,15 @@ import java.util.Set;
import javax.servlet.Filter; 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.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; 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.EndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer;
import org.springframework.boot.actuate.endpoint.mvc.ManagementErrorEndpoint; import org.springframework.boot.actuate.endpoint.mvc.ManagementErrorEndpoint;
@ -62,6 +65,9 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
@Configuration @Configuration
public class EndpointWebMvcChildContextConfiguration { public class EndpointWebMvcChildContextConfiguration {
private static Log logger = LogFactory
.getLog(EndpointWebMvcChildContextConfiguration.class);
@Value("${error.path:/error}") @Value("${error.path:/error}")
private String errorPath = "/error"; private String errorPath = "/error";
@ -135,6 +141,7 @@ public class EndpointWebMvcChildContextConfiguration {
EndpointHandlerMapping mapping = new EndpointHandlerMapping(set); EndpointHandlerMapping mapping = new EndpointHandlerMapping(set);
// In a child context we definitely want to see the parent endpoints // In a child context we definitely want to see the parent endpoints
mapping.setDetectHandlerMethodsInAncestorContexts(true); mapping.setDetectHandlerMethodsInAncestorContexts(true);
injectIntoSecurityFilter(beanFactory, mapping);
if (this.mappingCustomizers != null) { if (this.mappingCustomizers != null) {
for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) { for (EndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) {
customizer.customize(mapping); customizer.customize(mapping);
@ -143,6 +150,23 @@ public class EndpointWebMvcChildContextConfiguration {
return mapping; 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 * The error controller is present but not mapped as an endpoint in this context
* because of the DispatcherServlet having had it's HandlerMapping explicitly * because of the DispatcherServlet having had it's HandlerMapping explicitly

@ -22,10 +22,10 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.Endpoint; 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.EndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; 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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; 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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -215,6 +217,11 @@ public class ManagementSecurityAutoConfiguration {
@Autowired(required = false) @Autowired(required = false)
private EndpointHandlerMapping endpointHandlerMapping; private EndpointHandlerMapping endpointHandlerMapping;
public void setEndpointHandlerMapping(
EndpointHandlerMapping endpointHandlerMapping) {
this.endpointHandlerMapping = endpointHandlerMapping;
}
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
@ -230,8 +237,15 @@ public class ManagementSecurityAutoConfiguration {
http.requestMatchers().antMatchers(paths); http.requestMatchers().antMatchers(paths);
String[] endpointPaths = this.server.getPathsArray(getEndpointPaths( String[] endpointPaths = this.server.getPathsArray(getEndpointPaths(
this.endpointHandlerMapping, false)); this.endpointHandlerMapping, false));
http.authorizeRequests().antMatchers(endpointPaths).access("permitAll()") ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http
.anyRequest().hasRole(this.management.getSecurity().getRole()); .authorizeRequests();
authorizeRequests.antMatchers(endpointPaths).permitAll();
if (this.endpointHandlerMapping != null) {
authorizeRequests.requestMatchers(
new PrincipalHandlerRequestMatcher()).permitAll();
}
authorizeRequests.anyRequest().hasRole(
this.management.getSecurity().getRole());
http.httpBasic(); http.httpBasic();
// No cookies for management endpoints by default // No cookies for management endpoints by default
@ -252,6 +266,14 @@ public class ManagementSecurityAutoConfiguration {
return entryPoint; 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) { private static String[] getEndpointPaths(EndpointHandlerMapping endpointHandlerMapping) {
@ -269,8 +291,7 @@ public class ManagementSecurityAutoConfiguration {
Set<? extends MvcEndpoint> endpoints = endpointHandlerMapping.getEndpoints(); Set<? extends MvcEndpoint> endpoints = endpointHandlerMapping.getEndpoints();
List<String> paths = new ArrayList<String>(endpoints.size()); List<String> paths = new ArrayList<String>(endpoints.size());
for (MvcEndpoint endpoint : endpoints) { for (MvcEndpoint endpoint : endpoints) {
if (endpoint.isSensitive() == secure if (endpoint.isSensitive() == secure) {
|| (!secure && endpoint instanceof AnonymouslyAccessibleMvcEndpoint)) {
String path = endpointHandlerMapping.getPath(endpoint.getPath()); String path = endpointHandlerMapping.getPath(endpoint.getPath());
paths.add(path); paths.add(path);
// Add Spring MVC-generated additional paths // Add Spring MVC-generated additional paths

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

@ -17,15 +17,20 @@
package org.springframework.boot.actuate.endpoint.mvc; package org.springframework.boot.actuate.endpoint.mvc;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.security.Principal;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; 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.HandlerMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
@ -56,6 +61,8 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping impleme
private boolean disabled = false; private boolean disabled = false;
private Set<HandlerMethod> principalHandlers = new HashSet<HandlerMethod>();
/** /**
* Create a new {@link EndpointHandlerMapping} instance. All {@link Endpoint}s will be * Create a new {@link EndpointHandlerMapping} instance. All {@link Endpoint}s will be
* detected from the {@link ApplicationContext}. * detected from the {@link ApplicationContext}.
@ -127,9 +134,33 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping impleme
mapping.getHeadersCondition(), mapping.getConsumesCondition(), mapping.getHeadersCondition(), mapping.getConsumesCondition(),
mapping.getProducesCondition(), mapping.getCustomCondition()); mapping.getProducesCondition(), mapping.getCustomCondition());
if (handlesPrincipal(method)) {
this.principalHandlers.add(new HandlerMethod(handler, method));
}
super.registerHandlerMethod(handler, method, modified); 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 * @param prefix the prefix to set
*/ */

@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 1.1.0 * @since 1.1.0
*/ */
public class HealthMvcEndpoint implements AnonymouslyAccessibleMvcEndpoint { public class HealthMvcEndpoint implements MvcEndpoint {
private Map<String, HttpStatus> statusMapping = new HashMap<String, HttpStatus>(); private Map<String, HttpStatus> statusMapping = new HashMap<String, HttpStatus>();

@ -69,5 +69,8 @@ public class EndpointsPropertiesSampleActuatorApplicationTests {
assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals(HttpStatus.OK, entity.getStatusCode());
assertTrue("Wrong body: " + entity.getBody(), assertTrue("Wrong body: " + entity.getBody(),
entity.getBody().contains("\"status\":\"UP\"")); entity.getBody().contains("\"status\":\"UP\""));
System.err.println(entity.getBody());
assertTrue("Wrong body: " + entity.getBody(),
entity.getBody().contains("\"hello\":\"world\""));
} }
} }

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