Hone the security rules for actuator endpoints

Takes into account the fact that the new /actuator endpoint sometimes
loses its path (it is "" relative to a non-empty management context path).

Fixes gh-4059
This commit is contained in:
Dave Syer 2015-10-01 13:30:26 +01:00
parent 15f22651f8
commit eb29847814
3 changed files with 145 additions and 59 deletions

View File

@ -101,8 +101,8 @@ public class ManagementWebSecurityAutoConfiguration {
} }
@Configuration @Configuration
protected static class ManagementSecurityPropertiesConfiguration implements protected static class ManagementSecurityPropertiesConfiguration
SecurityPrerequisite { implements SecurityPrerequisite {
@Autowired(required = false) @Autowired(required = false)
private SecurityProperties security; private SecurityProperties security;
@ -122,8 +122,8 @@ public class ManagementWebSecurityAutoConfiguration {
// Get the ignored paths in early // Get the ignored paths in early
@Order(SecurityProperties.IGNORED_ORDER + 1) @Order(SecurityProperties.IGNORED_ORDER + 1)
private static class IgnoredPathsWebSecurityConfigurerAdapter implements private static class IgnoredPathsWebSecurityConfigurerAdapter
WebSecurityConfigurer<WebSecurity> { implements WebSecurityConfigurer<WebSecurity> {
@Autowired(required = false) @Autowired(required = false)
private ErrorController errorController; private ErrorController errorController;
@ -152,8 +152,8 @@ public class ManagementWebSecurityAutoConfiguration {
List<String> ignored = SpringBootWebSecurityConfiguration List<String> ignored = SpringBootWebSecurityConfiguration
.getIgnored(this.security); .getIgnored(this.security);
if (!this.management.getSecurity().isEnabled()) { if (!this.management.getSecurity().isEnabled()) {
ignored.addAll(Arrays.asList(EndpointPaths ignored.addAll(
.get(this.endpointHandlerMapping))); Arrays.asList(EndpointPaths.get(this.endpointHandlerMapping)));
} }
if (ignored.contains("none")) { if (ignored.contains("none")) {
ignored.remove("none"); ignored.remove("none");
@ -192,12 +192,13 @@ public class ManagementWebSecurityAutoConfiguration {
@Override @Override
public ConditionOutcome getMatchOutcome(ConditionContext context, public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) { AnnotatedTypeMetadata metadata) {
String managementEnabled = context.getEnvironment().getProperty( String managementEnabled = context.getEnvironment()
"management.security.enabled", "true"); .getProperty("management.security.enabled", "true");
String basicEnabled = context.getEnvironment().getProperty( String basicEnabled = context.getEnvironment()
"security.basic.enabled", "true"); .getProperty("security.basic.enabled", "true");
return new ConditionOutcome("true".equalsIgnoreCase(managementEnabled) return new ConditionOutcome(
&& !"true".equalsIgnoreCase(basicEnabled), "true".equalsIgnoreCase(managementEnabled)
&& !"true".equalsIgnoreCase(basicEnabled),
"Management security enabled and basic disabled"); "Management security enabled and basic disabled");
} }
@ -207,8 +208,8 @@ public class ManagementWebSecurityAutoConfiguration {
@ConditionalOnMissingBean({ ManagementWebSecurityConfigurerAdapter.class }) @ConditionalOnMissingBean({ ManagementWebSecurityConfigurerAdapter.class })
@ConditionalOnProperty(prefix = "management.security", name = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "management.security", name = "enabled", matchIfMissing = true)
@Order(ManagementServerProperties.BASIC_AUTH_ORDER) @Order(ManagementServerProperties.BASIC_AUTH_ORDER)
protected static class ManagementWebSecurityConfigurerAdapter extends protected static class ManagementWebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter { extends WebSecurityConfigurerAdapter {
@Autowired @Autowired
private SecurityProperties security; private SecurityProperties security;
@ -234,14 +235,14 @@ public class ManagementWebSecurityAutoConfiguration {
if (this.endpointHandlerMapping == null) { if (this.endpointHandlerMapping == null) {
ApplicationContext context = (this.contextResolver == null ? null ApplicationContext context = (this.contextResolver == null ? null
: this.contextResolver.getApplicationContext()); : this.contextResolver.getApplicationContext());
if (context != null if (context != null && context
&& context.getBeanNamesForType(EndpointHandlerMapping.class).length > 0) { .getBeanNamesForType(EndpointHandlerMapping.class).length > 0) {
this.endpointHandlerMapping = context this.endpointHandlerMapping = context
.getBean(EndpointHandlerMapping.class); .getBean(EndpointHandlerMapping.class);
} }
if (this.endpointHandlerMapping == null) { if (this.endpointHandlerMapping == null) {
this.endpointHandlerMapping = new EndpointHandlerMapping( this.endpointHandlerMapping = new EndpointHandlerMapping(
Collections.<MvcEndpoint>emptySet()); Collections.<MvcEndpoint> emptySet());
} }
} }
} }
@ -257,9 +258,10 @@ public class ManagementWebSecurityAutoConfiguration {
} }
AuthenticationEntryPoint entryPoint = entryPoint(); AuthenticationEntryPoint entryPoint = entryPoint();
http.exceptionHandling().authenticationEntryPoint(entryPoint); http.exceptionHandling().authenticationEntryPoint(entryPoint);
// Match all the requests for actuator endpoints ...
http.requestMatcher(matcher); http.requestMatcher(matcher);
configureAuthorizeRequests(new EndpointPathRequestMatcher(false), // ... but permitAll() for the non-sensitive ones
http.authorizeRequests()); configurePermittedRequests(http.authorizeRequests());
http.httpBasic().authenticationEntryPoint(entryPoint); http.httpBasic().authenticationEntryPoint(entryPoint);
// No cookies for management endpoints by default // No cookies for management endpoints by default
http.csrf().disable(); http.csrf().disable();
@ -280,7 +282,9 @@ public class ManagementWebSecurityAutoConfiguration {
this.server.getPath(path) + "/**"); this.server.getPath(path) + "/**");
return matcher; return matcher;
} }
return new EndpointPathRequestMatcher(); // Match everything, including the sensitive and non-sensitive paths
return new EndpointPathRequestMatcher(
EndpointPaths.get(this.endpointHandlerMapping));
} }
private AuthenticationEntryPoint entryPoint() { private AuthenticationEntryPoint entryPoint() {
@ -289,25 +293,23 @@ public class ManagementWebSecurityAutoConfiguration {
return entryPoint; return entryPoint;
} }
private void configureAuthorizeRequests( private void configurePermittedRequests(
RequestMatcher permitAllMatcher,
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry requests) { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry requests) {
requests.requestMatchers(permitAllMatcher).permitAll(); // Permit access to the non-sensitive endpoints
requests.requestMatchers(new EndpointPathRequestMatcher(
EndpointPaths.get(this.endpointHandlerMapping, false))).permitAll();
// Restrict the rest to the configured role
requests.anyRequest().hasRole(this.management.getSecurity().getRole()); requests.anyRequest().hasRole(this.management.getSecurity().getRole());
} }
private final class EndpointPathRequestMatcher implements RequestMatcher { private final class EndpointPathRequestMatcher implements RequestMatcher {
private boolean sensitive;
private RequestMatcher delegate; private RequestMatcher delegate;
EndpointPathRequestMatcher() { private String[] paths;
this(true);
}
EndpointPathRequestMatcher(boolean sensitive) { EndpointPathRequestMatcher(String[] paths) {
this.sensitive = sensitive; this.paths = paths;
} }
@Override @Override
@ -323,33 +325,41 @@ public class ManagementWebSecurityAutoConfiguration {
private RequestMatcher createDelegate() { private RequestMatcher createDelegate() {
ServerProperties server = ManagementWebSecurityConfigurerAdapter.this.server; ServerProperties server = ManagementWebSecurityConfigurerAdapter.this.server;
List<RequestMatcher> matchers = new ArrayList<RequestMatcher>(); List<RequestMatcher> matchers = new ArrayList<RequestMatcher>();
for (String path : getPaths()) { for (String path : this.paths) {
matchers.add(new AntPathRequestMatcher(server.getPath(path))); matchers.add(new AntPathRequestMatcher(server.getPath(path)));
} }
return (matchers.isEmpty() ? AnyRequestMatcher.INSTANCE return (matchers.isEmpty() ? AnyRequestMatcher.INSTANCE
: new OrRequestMatcher(matchers)); : new OrRequestMatcher(matchers));
} }
private String[] getPaths() {
EndpointHandlerMapping endpointHandlerMapping = ManagementWebSecurityConfigurerAdapter.this.endpointHandlerMapping;
if (this.sensitive) {
return EndpointPaths.get(endpointHandlerMapping);
}
return EndpointPaths.get(endpointHandlerMapping, false);
}
} }
} }
/**
* Helper class for extracting lists of paths from the EndpointHandlerMapping.
*/
private static class EndpointPaths { private static class EndpointPaths {
/**
* Get all the paths (sensitive and unsensitive).
*
* @param endpointHandlerMapping the mapping
* @return all paths
*/
public static String[] get(EndpointHandlerMapping endpointHandlerMapping) { public static String[] get(EndpointHandlerMapping endpointHandlerMapping) {
String[] insecure = get(endpointHandlerMapping, false); String[] insecure = get(endpointHandlerMapping, false);
String[] secure = get(endpointHandlerMapping, true); String[] secure = get(endpointHandlerMapping, true);
return StringUtils.mergeStringArrays(insecure, secure); return StringUtils.mergeStringArrays(insecure, secure);
} }
/**
* Get all the paths that are either sensitive or unsensitive.
*
* @param endpointHandlerMapping the mapping
* @param secure flag to say if we want the secure ones
* @return the relevant paths
*/
public static String[] get(EndpointHandlerMapping endpointHandlerMapping, public static String[] get(EndpointHandlerMapping endpointHandlerMapping,
boolean secure) { boolean secure) {
if (endpointHandlerMapping == null) { if (endpointHandlerMapping == null) {
@ -362,14 +372,14 @@ public class ManagementWebSecurityAutoConfiguration {
String path = endpointHandlerMapping.getPath(endpoint.getPath()); String path = endpointHandlerMapping.getPath(endpoint.getPath());
paths.add(path); paths.add(path);
if (!path.equals("")) { if (!path.equals("")) {
// Ensure that nested paths are secured if (secure) {
paths.add(path + "/**"); // Ensure that nested paths are secured
// Add Spring MVC-generated additional paths paths.add(path + "/**");
paths.add(path + ".*"); // Add Spring MVC-generated additional paths
} paths.add(path + ".*");
else { }
paths.add("/");
} }
paths.add(path + "/");
} }
} }
return paths.toArray(new String[paths.size()]); return paths.toArray(new String[paths.size()]);

View File

@ -16,6 +16,13 @@
package org.springframework.boot.actuate.endpoint.mvc; package org.springframework.boot.actuate.endpoint.mvc;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration;
@ -32,18 +39,14 @@ import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** /**
* Integration tests for the Actuator's MVC endpoints. * Integration tests for the Actuator's MVC endpoints.
* *
@ -55,6 +58,11 @@ public class MvcEndpointIntegrationTests {
private AnnotationConfigWebApplicationContext context; private AnnotationConfigWebApplicationContext context;
@After
public void close() {
TestSecurityContextHolder.clearContext();
}
@Test @Test
public void defaultJsonResponseIsNotIndented() throws Exception { public void defaultJsonResponseIsNotIndented() throws Exception {
this.context = new AnnotationConfigWebApplicationContext(); this.context = new AnnotationConfigWebApplicationContext();
@ -81,13 +89,80 @@ public class MvcEndpointIntegrationTests {
} }
@Test @Test
public void endpointsAreSecureByDefault() throws Exception { public void nonSensitiveEndpointsAreNotSecureByDefault() throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/info")).andExpect(status().isOk());
mockMvc.perform(get("/actuator")).andExpect(status().isOk());
}
@Test
public void nonSensitiveEndpointsAreNotSecureByDefaultWithCustomContextPath()
throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"management.context-path:/management");
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/info")).andExpect(status().isOk());
mockMvc.perform(get("/management/")).andExpect(status().isOk());
}
@Test
public void sensitiveEndpointsAreSecureByDefault() throws Exception {
this.context = new AnnotationConfigWebApplicationContext(); this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class); this.context.register(SecureConfiguration.class);
MockMvc mockMvc = createSecureMockMvc(); MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/beans")).andExpect(status().isUnauthorized()); mockMvc.perform(get("/beans")).andExpect(status().isUnauthorized());
} }
@Test
public void sensitiveEndpointsAreSecureByDefaultWithCustomContextPath()
throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"management.context-path:/management");
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans")).andExpect(status().isUnauthorized());
}
@Test
public void sensitiveEndpointsAreSecureWithNonAdminRoleWithCustomContextPath()
throws Exception {
TestSecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("user", "N/A", "ROLE_USER"));
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"management.context-path:/management");
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans")).andExpect(status().isForbidden());
}
@Test
public void sensitiveEndpointsAreSecureWithAdminRoleWithCustomContextPath()
throws Exception {
TestSecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("user", "N/A", "ROLE_ADMIN"));
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"management.context-path:/management");
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans")).andExpect(status().isOk());
}
@Test
public void endpointSecurityCanBeDisabledWithCustomContextPath() throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"management.context-path:/management",
"management.security.enabled:false");
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/beans")).andExpect(status().isOk());
}
@Test @Test
public void endpointSecurityCanBeDisabled() throws Exception { public void endpointSecurityCanBeDisabled() throws Exception {
this.context = new AnnotationConfigWebApplicationContext(); this.context = new AnnotationConfigWebApplicationContext();
@ -104,8 +179,8 @@ public class MvcEndpointIntegrationTests {
EnvironmentTestUtils.addEnvironment(this.context, EnvironmentTestUtils.addEnvironment(this.context,
"spring.jackson.serialization.indent-output:true"); "spring.jackson.serialization.indent-output:true");
MockMvc mockMvc = createMockMvc(); MockMvc mockMvc = createMockMvc();
mockMvc.perform(get("/beans")).andExpect( mockMvc.perform(get("/beans"))
content().string(startsWith("{" + LINE_SEPARATOR))); .andExpect(content().string(startsWith("{" + LINE_SEPARATOR)));
} }
private MockMvc createMockMvc() { private MockMvc createMockMvc() {
@ -127,8 +202,8 @@ public class MvcEndpointIntegrationTests {
} }
@ImportAutoConfiguration({ JacksonAutoConfiguration.class, @ImportAutoConfiguration({ JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class })
static class DefaultConfiguration { static class DefaultConfiguration {
@ -146,8 +221,8 @@ public class MvcEndpointIntegrationTests {
@ImportAutoConfiguration({ HypermediaAutoConfiguration.class, @ImportAutoConfiguration({ HypermediaAutoConfiguration.class,
RepositoryRestMvcAutoConfiguration.class, JacksonAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class, JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class }) PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class })
static class SpringDataRestConfiguration { static class SpringDataRestConfiguration {

View File

@ -4,4 +4,5 @@
<root level="INFO"> <root level="INFO">
<appender-ref ref="CONSOLE" /> <appender-ref ref="CONSOLE" />
</root> </root>
<logger name="org.springframework.security" level="DEBUG"/>
</configuration> </configuration>