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
|
||||
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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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\""));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
error.path: /oops
|
||||
management.contextPath: /admin
|
||||
management.contextPath: /admin
|
||||
endpoints.health.sensitive: false
|
Loading…
Reference in New Issue