diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/SecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/SecurityAutoConfiguration.java index cb84c55e94..c50c8f4479 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/SecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/SecurityAutoConfiguration.java @@ -98,6 +98,8 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher; @EnableConfigurationProperties public class SecurityAutoConfiguration { + private static final String[] NO_PATHS = new String[0]; + @Bean(name = "org.springframework.actuate.properties.SecurityProperties") @ConditionalOnMissingBean public SecurityProperties securityProperties() { @@ -119,6 +121,7 @@ public class SecurityAutoConfiguration { @Bean @ConditionalOnMissingBean({ ManagementWebSecurityConfigurerAdapter.class }) + @ConditionalOnExpression("${security.management.enabled:true}") public WebSecurityConfigurerAdapter managementWebSecurityConfigurerAdapter() { return new ManagementWebSecurityConfigurerAdapter(); } @@ -140,6 +143,9 @@ public class SecurityAutoConfiguration { @Autowired(required = false) private ErrorController errorController; + @Autowired(required = false) + private EndpointHandlerMapping endpointHandlerMapping; + @Override protected void configure(HttpSecurity http) throws Exception { @@ -191,6 +197,10 @@ public class SecurityAutoConfiguration { public void configure(WebSecurity builder) throws Exception { IgnoredRequestConfigurer ignoring = builder.ignoring(); List ignored = new ArrayList(this.security.getIgnored()); + if (!this.security.getManagement().isEnabled()) { + ignored.addAll(Arrays.asList(getEndpointPaths( + this.endpointHandlerMapping, true))); + } if (ignored.isEmpty()) { ignored.addAll(DEFAULT_IGNORED); } @@ -220,8 +230,6 @@ public class SecurityAutoConfiguration { private static class ManagementWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - private static final String[] NO_PATHS = new String[0]; - @Autowired private SecurityProperties security; @@ -234,7 +242,8 @@ public class SecurityAutoConfiguration { @Override protected void configure(HttpSecurity http) throws Exception { - String[] paths = getEndpointPaths(true); // secure endpoints + // secure endpoints + String[] paths = getEndpointPaths(this.endpointHandlerMapping, true); if (paths.length > 0 && this.security.getManagement().isEnabled()) { // Always protect them if present if (this.security.isRequireSsl()) { @@ -262,7 +271,7 @@ public class SecurityAutoConfiguration { @Override public void configure(WebSecurity builder) throws Exception { IgnoredRequestConfigurer ignoring = builder.ignoring(); - ignoring.antMatchers(getEndpointPaths(false)); + ignoring.antMatchers(getEndpointPaths(this.endpointHandlerMapping, false)); } private AuthenticationEntryPoint entryPoint() { @@ -271,21 +280,6 @@ public class SecurityAutoConfiguration { return entryPoint; } - private String[] getEndpointPaths(boolean secure) { - if (this.endpointHandlerMapping == null) { - return NO_PATHS; - } - - List> endpoints = this.endpointHandlerMapping.getEndpoints(); - List paths = new ArrayList(endpoints.size()); - for (Endpoint endpoint : endpoints) { - if (endpoint.isSensitive() == secure) { - paths.add(endpoint.getPath()); - } - } - return paths.toArray(new String[paths.size()]); - } - } @ConditionalOnMissingBean(AuthenticationManager.class) @@ -299,7 +293,8 @@ public class SecurityAutoConfiguration { private SecurityProperties security; @Bean - public AuthenticationManager authenticationManager(ObjectPostProcessor objectPostProcessor) throws Exception { + public AuthenticationManager authenticationManager( + ObjectPostProcessor objectPostProcessor) throws Exception { InMemoryUserDetailsManagerConfigurer builder = new AuthenticationManagerBuilder( objectPostProcessor).inMemoryAuthentication(); @@ -322,6 +317,22 @@ public class SecurityAutoConfiguration { } + private static String[] getEndpointPaths( + EndpointHandlerMapping endpointHandlerMapping, boolean secure) { + if (endpointHandlerMapping == null) { + return NO_PATHS; + } + + List> endpoints = endpointHandlerMapping.getEndpoints(); + List paths = new ArrayList(endpoints.size()); + for (Endpoint endpoint : endpoints) { + if (endpoint.isSensitive() == secure) { + paths.add(endpoint.getPath()); + } + } + return paths.toArray(new String[paths.size()]); + } + private static void configureHeaders(HeadersConfigurer configurer, SecurityProperties.Headers headers) throws Exception { if (headers.getHsts() != Headers.HSTS.none) { diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml index 97f4911d2b..d1900162b3 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/resources/logback.xml @@ -2,4 +2,5 @@ + diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/org/springframework/boot/sample/ops/UnsecureManagementSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/org/springframework/boot/sample/ops/UnsecureManagementSampleActuatorApplicationTests.java new file mode 100644 index 0000000000..3e6082a339 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/org/springframework/boot/sample/ops/UnsecureManagementSampleActuatorApplicationTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2013 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.sample.ops; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.RestTemplate; + +/** + * Integration tests for unsecured service endpoints (even with Spring Security on + * classpath). + * + * @author Dave Syer + */ +public class UnsecureManagementSampleActuatorApplicationTests { + + private static ConfigurableApplicationContext context; + + @BeforeClass + public static void start() throws Exception { + Future future = Executors + .newSingleThreadExecutor().submit( + new Callable() { + @Override + public ConfigurableApplicationContext call() throws Exception { + return (ConfigurableApplicationContext) SpringApplication + .run(SampleActuatorApplication.class, + "--security.management.enabled=false"); + } + }); + context = future.get(60, TimeUnit.SECONDS); + } + + @AfterClass + public static void stop() { + if (context != null) { + context.close(); + } + } + + @Test + public void testHomeIsSecure() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = getRestTemplate().getForEntity( + "http://localhost:8080", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, entity.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = entity.getBody(); + assertEquals("Wrong body: " + body, "Unauthorized", body.get("error")); + assertFalse("Wrong headers: " + entity.getHeaders(), entity.getHeaders() + .containsKey("Set-Cookie")); + } + + @Test + public void testMetrics() throws Exception { + try { + testHomeIsSecure(); // makes sure some requests have been made + } catch (AssertionError e) { + // ignore; + } + @SuppressWarnings("rawtypes") + ResponseEntity entity = getRestTemplate().getForEntity( + "http://localhost:8080/metrics", Map.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = entity.getBody(); + assertTrue("Wrong body: " + body, body.containsKey("counter.status.401.root")); + } + + private RestTemplate getRestTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + public void handleError(ClientHttpResponse response) throws IOException { + } + }); + return restTemplate; + + } + +}