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:
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…
Reference in New Issue