/*
* Copyright 2012-2017 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.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.security.AuthenticationManagerConfiguration;
import org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.IgnoredRequestCustomizer;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityPrerequisite;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.security.SpringBootWebSecurityConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity.IgnoredRequestConfigurer;
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.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for security of framework endpoints.
* Many aspects of the behavior can be controller with {@link ManagementServerProperties}
* via externalized application properties (or via an bean definition of that type to set
* the defaults).
* <p>
* The framework {@link Endpoint}s (used to expose application information to operations)
* include a {@link Endpoint#isSensitive() sensitive} configuration option which will be
* used as a security hint by the filter created here.
*
* @author Dave Syer
* @author Andy Wilkinson
*/
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ EnableWebSecurity.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
@AutoConfigureBefore(FallbackWebSecurityAutoConfiguration.class)
@EnableConfigurationProperties(ManagementServerProperties.class)
public class ManagementWebSecurityAutoConfiguration {
private static final String[] NO_PATHS = new String[0];
private static final RequestMatcher MATCH_NONE = new NegatedRequestMatcher(
AnyRequestMatcher.INSTANCE);
@Bean
public IgnoredRequestCustomizer managementIgnoredRequestCustomizer(
ManagementServerProperties management,
ObjectProvider<ManagementContextResolver> contextResolver) {
return new ManagementIgnoredRequestCustomizer(management,
contextResolver.getIfAvailable());
}
private class ManagementIgnoredRequestCustomizer implements IgnoredRequestCustomizer {
private final ManagementServerProperties management;
private final ManagementContextResolver contextResolver;
ManagementIgnoredRequestCustomizer(ManagementServerProperties management,
ManagementContextResolver contextResolver) {
this.management = management;
this.contextResolver = contextResolver;
}
@Override
public void customize(IgnoredRequestConfigurer configurer) {
if (!this.management.getSecurity().isEnabled()) {
RequestMatcher requestMatcher = LazyEndpointPathRequestMatcher
.getRequestMatcher(this.contextResolver);
configurer.requestMatchers(requestMatcher);
}
}
}
@Configuration
protected static class ManagementSecurityPropertiesConfiguration
implements SecurityPrerequisite {
private final SecurityProperties securityProperties;
private final ManagementServerProperties managementServerProperties;
public ManagementSecurityPropertiesConfiguration(
ObjectProvider<SecurityProperties> securityProperties,
ObjectProvider<ManagementServerProperties> managementServerProperties) {
this.securityProperties = securityProperties.getIfAvailable();
this.managementServerProperties = managementServerProperties.getIfAvailable();
}
@PostConstruct
public void init() {
if (this.managementServerProperties != null
&& this.securityProperties != null) {
this.securityProperties.getUser().getRole()
.addAll(this.managementServerProperties.getSecurity().getRoles());
}
}
}
@Configuration
@ConditionalOnMissingBean(WebSecurityConfiguration.class)
@Conditional(WebSecurityEnablerCondition.class)
@EnableWebSecurity
protected static class WebSecurityEnabler extends AuthenticationManagerConfiguration {
}
/**
* WebSecurityEnabler condition.
*/
static class WebSecurityEnablerCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
String managementEnabled = context.getEnvironment()
.getProperty("management.security.enabled", "true");
String basicEnabled = context.getEnvironment()
.getProperty("security.basic.enabled", "true");
ConditionMessage.Builder message = ConditionMessage
.forCondition("WebSecurityEnabled");
if ("true".equalsIgnoreCase(managementEnabled)
&& !"true".equalsIgnoreCase(basicEnabled)) {
return ConditionOutcome.match(message.because("security enabled"));
}
return ConditionOutcome.noMatch(message.because("security disabled"));
}
}
@Configuration
@ConditionalOnMissingBean({ ManagementWebSecurityConfigurerAdapter.class })
@ConditionalOnProperty(prefix = "management.security", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(SecurityProperties.class)
@Order(ManagementServerProperties.BASIC_AUTH_ORDER)
protected static class ManagementWebSecurityConfigurerAdapter
extends WebSecurityConfigurerAdapter {
private final SecurityProperties security;
private final ManagementServerProperties management;
private final ManagementContextResolver contextResolver;
public ManagementWebSecurityConfigurerAdapter(SecurityProperties security,
ManagementServerProperties management,
ObjectProvider<ManagementContextResolver> contextResolver) {
this.security = security;
this.management = management;
this.contextResolver = contextResolver.getIfAvailable();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// secure endpoints
RequestMatcher matcher = getRequestMatcher();
if (matcher != null) {
// Always protect them if present
if (this.security.isRequireSsl()) {
http.requiresChannel().anyRequest().requiresSecure();
}
AuthenticationEntryPoint entryPoint = entryPoint();
http.exceptionHandling().authenticationEntryPoint(entryPoint);
// Match all the requests for actuator endpoints ...
http.requestMatcher(matcher);
// ... but permitAll() for the non-sensitive ones
configurePermittedRequests(http.authorizeRequests());
http.httpBasic().authenticationEntryPoint(entryPoint);
// No cookies for management endpoints by default
http.csrf().disable();
http.sessionManagement()
.sessionCreationPolicy(asSpringSecuritySessionCreationPolicy(
this.management.getSecurity().getSessions()));
SpringBootWebSecurityConfiguration.configureHeaders(http.headers(),
this.security.getHeaders());
}
}
private SessionCreationPolicy asSpringSecuritySessionCreationPolicy(
Enum<?> value) {
if (value == null) {
return SessionCreationPolicy.STATELESS;
}
return SessionCreationPolicy.valueOf(value.name());
}
private RequestMatcher getRequestMatcher() {
if (this.management.getSecurity().isEnabled()) {
return LazyEndpointPathRequestMatcher
.getRequestMatcher(this.contextResolver);
}
return null;
}
private AuthenticationEntryPoint entryPoint() {
BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint();
entryPoint.setRealmName(this.security.getBasic().getRealm());
return entryPoint;
}
private void configurePermittedRequests(
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry requests) {
requests.requestMatchers(new LazyEndpointPathRequestMatcher(
this.contextResolver, EndpointPaths.SENSITIVE)).authenticated();
// Permit access to the non-sensitive endpoints
requests.requestMatchers(new LazyEndpointPathRequestMatcher(
this.contextResolver, EndpointPaths.NON_SENSITIVE)).permitAll();
}
}
private enum EndpointPaths {
ALL,
NON_SENSITIVE {
@Override
protected boolean isIncluded(MvcEndpoint endpoint) {
return !endpoint.isSensitive();
}
},
SENSITIVE {
@Override
protected boolean isIncluded(MvcEndpoint endpoint) {
return endpoint.isSensitive();
}
};
public String[] getPaths(EndpointHandlerMapping endpointHandlerMapping) {
if (endpointHandlerMapping == null) {
return NO_PATHS;
}
Set<? extends MvcEndpoint> endpoints = endpointHandlerMapping.getEndpoints();
Set<String> paths = new LinkedHashSet<>(endpoints.size());
for (MvcEndpoint endpoint : endpoints) {
if (isIncluded(endpoint)) {
String path = endpointHandlerMapping.getPath(endpoint.getPath());
paths.add(path);
if (!path.equals("")) {
paths.add(path + "/**");
// Add Spring MVC-generated additional paths
paths.add(path + ".*");
}
paths.add(path + "/");
}
}
return paths.toArray(new String[paths.size()]);
}
protected boolean isIncluded(MvcEndpoint endpoint) {
return true;
}
}
private static class LazyEndpointPathRequestMatcher implements RequestMatcher {
private final EndpointPaths endpointPaths;
private final ManagementContextResolver contextResolver;
private RequestMatcher delegate;
public static RequestMatcher getRequestMatcher(
ManagementContextResolver contextResolver) {
if (contextResolver == null) {
return null;
}
ManagementServerProperties management = contextResolver
.getApplicationContext().getBean(ManagementServerProperties.class);
ServerProperties server = contextResolver.getApplicationContext()
.getBean(ServerProperties.class);
String path = management.getContextPath();
if (StringUtils.hasText(path)) {
AntPathRequestMatcher matcher = new AntPathRequestMatcher(
server.getServlet().getPath(path) + "/**");
return matcher;
}
// Match everything, including the sensitive and non-sensitive paths
return new LazyEndpointPathRequestMatcher(contextResolver, EndpointPaths.ALL);
}
LazyEndpointPathRequestMatcher(ManagementContextResolver contextResolver,
EndpointPaths endpointPaths) {
this.contextResolver = contextResolver;
this.endpointPaths = endpointPaths;
}
@Override
public boolean matches(HttpServletRequest request) {
if (this.delegate == null) {
this.delegate = createDelegate();
}
return this.delegate.matches(request);
}
private RequestMatcher createDelegate() {
ServerProperties server = this.contextResolver.getApplicationContext()
.getBean(ServerProperties.class);
List<RequestMatcher> matchers = new ArrayList<>();
EndpointHandlerMapping endpointHandlerMapping = getRequiredEndpointHandlerMapping();
for (String path : this.endpointPaths.getPaths(endpointHandlerMapping)) {
matchers.add(
new AntPathRequestMatcher(server.getServlet().getPath(path)));
}
return (matchers.isEmpty() ? MATCH_NONE : new OrRequestMatcher(matchers));
}
private EndpointHandlerMapping getRequiredEndpointHandlerMapping() {
EndpointHandlerMapping endpointHandlerMapping = null;
ApplicationContext context = this.contextResolver.getApplicationContext();
if (context.getBeanNamesForType(EndpointHandlerMapping.class).length > 0) {
endpointHandlerMapping = context.getBean(EndpointHandlerMapping.class);
}
if (endpointHandlerMapping == null) {
// Maybe there are actually no endpoints (e.g. management.port=-1)
endpointHandlerMapping = new EndpointHandlerMapping(
Collections.<MvcEndpoint>emptySet());
}
return endpointHandlerMapping;
}
}
}