diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java index 49228c0b8c7..1b405f1dd40 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java @@ -41,6 +41,7 @@ import org.springframework.boot.autoconfigure.security.SecurityPrequisite; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SpringBootWebSecurityConfiguration; import org.springframework.boot.autoconfigure.web.ErrorController; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -121,6 +122,9 @@ public class ManagementSecurityAutoConfiguration { @Autowired private SecurityProperties security; + @Autowired + private ServerProperties server; + @Override public void configure(WebSecurity builder) throws Exception { } @@ -144,7 +148,8 @@ public class ManagementSecurityAutoConfiguration { if (this.errorController != null) { ignored.add(normalizePath(this.errorController.getErrorPath())); } - ignoring.antMatchers(ignored.toArray(new String[0])); + String[] paths = this.server.getPathsArray(ignored); + ignoring.antMatchers(paths); } private String normalizePath(String errorPath) { @@ -178,6 +183,9 @@ public class ManagementSecurityAutoConfiguration { @Autowired private ManagementServerProperties management; + @Autowired + private ServerProperties server; + @Autowired(required = false) private EndpointHandlerMapping endpointHandlerMapping; @@ -192,6 +200,7 @@ public class ManagementSecurityAutoConfiguration { http.requiresChannel().anyRequest().requiresSecure(); } http.exceptionHandling().authenticationEntryPoint(entryPoint()); + paths = this.server.getPathsArray(paths); http.requestMatchers().antMatchers(paths); http.authorizeRequests().anyRequest() .hasRole(this.management.getSecurity().getRole()) // diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java index 0b26cf06b4c..246d526bd92 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.SecurityProperties.Headers; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; @@ -142,6 +143,9 @@ public class SpringBootWebSecurityConfiguration { @Autowired private SecurityProperties security; + @Autowired + private ServerProperties server; + @Override public void configure(WebSecurity builder) throws Exception { } @@ -150,7 +154,8 @@ public class SpringBootWebSecurityConfiguration { public void init(WebSecurity builder) throws Exception { IgnoredRequestConfigurer ignoring = builder.ignoring(); List ignored = getIgnored(this.security); - ignoring.antMatchers(ignored.toArray(new String[0])); + String[] paths = this.server.getPathsArray(ignored); + ignoring.antMatchers(paths); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 9293b2065df..a33608ac6d3 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.web; import java.io.File; import java.net.InetAddress; +import java.util.Collection; import javax.validation.constraints.NotNull; @@ -316,4 +317,30 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { } + public String[] getPathsArray(Collection paths) { + String[] result = new String[paths.size()]; + int i = 0; + for (String path : paths) { + result[i++] = getPath(path); + } + return result; + } + + public String[] getPathsArray(String[] paths) { + String[] result = new String[paths.length]; + int i = 0; + for (String path : paths) { + result[i++] = getPath(path); + } + return result; + } + + public String getPath(String path) { + String prefix = getServletPrefix(); + if (!path.startsWith("/")) { + path = "/" + path; + } + return prefix + path; + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java index e257d58f206..7cdfd469aec 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.test.City; +import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -55,6 +56,7 @@ public class SecurityAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); this.context.refresh(); assertNotNull(this.context.getBean(AuthenticationManagerBuilder.class)); @@ -69,6 +71,7 @@ public class SecurityAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "security.ignored:none"); this.context.refresh(); @@ -82,6 +85,7 @@ public class SecurityAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "security.basic.enabled:false"); this.context.refresh(); @@ -94,6 +98,7 @@ public class SecurityAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); this.context.refresh(); assertNotNull(this.context.getBean(AuthenticationManager.class)); @@ -104,6 +109,7 @@ public class SecurityAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(TestConfiguration.class, SecurityAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); this.context.refresh(); assertEquals(this.context.getBean(TestConfiguration.class).authenticationManager, @@ -119,7 +125,7 @@ public class SecurityAutoConfigurationTests { this.context.register(EntityConfiguration.class, PropertyPlaceholderAutoConfiguration.class, DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, - SecurityAutoConfiguration.class); + SecurityAutoConfiguration.class, ServerPropertiesAutoConfiguration.class); // This can fail if security @Conditionals force early instantiation of the // HibernateJpaAutoConfiguration (e.g. the EntityManagerFactory is not found) this.context.refresh(); diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathSampleActuatorApplicationTests.java new file mode 100644 index 00000000000..cd6a19b7e56 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathSampleActuatorApplicationTests.java @@ -0,0 +1,86 @@ +/* + * 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 sample.actuator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration tests for endpoints configuration. + * + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleActuatorApplication.class) +@WebAppConfiguration +@IntegrationTest({"server.port=0", "server.servletPath=/spring"}) +@DirtiesContext +public class ServletPathSampleActuatorApplicationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void testErrorPath() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = new TestRestTemplate("user", "password") + .getForEntity("http://localhost:" + this.port + "/spring/error", Map.class); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, entity.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = entity.getBody(); + assertEquals("None", body.get("error")); + assertEquals(999, body.get("status")); + } + + @Test + public void testHealth() throws Exception { + ResponseEntity entity = new TestRestTemplate().getForEntity( + "http://localhost:" + this.port + "/spring/health", String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains("\"status\":\"UP\"")); + } + + @Test + public void testHomeIsSecure() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = new TestRestTemplate().getForEntity( + "http://localhost:" + this.port + "/spring/", 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")); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathUnsecureSampleActuatorApplicationTests.java b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathUnsecureSampleActuatorApplicationTests.java new file mode 100644 index 00000000000..5189ce67ab2 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-actuator/src/test/java/sample/actuator/ServletPathUnsecureSampleActuatorApplicationTests.java @@ -0,0 +1,73 @@ +/* + * 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 sample.actuator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration tests for unsecured service endpoints (even with Spring Security on + * classpath). + * + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleActuatorApplication.class) +@WebAppConfiguration +@IntegrationTest({ "server.port:0", "security.basic.enabled:false", "server.servletPath:/spring" }) +@DirtiesContext +public class ServletPathUnsecureSampleActuatorApplicationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void testHome() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = new TestRestTemplate().getForEntity( + "http://localhost:" + this.port + "/spring/", Map.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = entity.getBody(); + assertEquals("Hello Phil", body.get("message")); + assertFalse("Wrong headers: " + entity.getHeaders(), entity.getHeaders() + .containsKey("Set-Cookie")); + } + + @Test + public void testMetricsIsSecure() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = new TestRestTemplate().getForEntity( + "http://localhost:" + this.port + "/spring/metrics", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, entity.getStatusCode()); + } + +}