diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java index 502e0bc425f..912a6b11248 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java @@ -23,6 +23,7 @@ import javax.servlet.Servlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.web.BasicErrorController; import org.springframework.boot.actuate.web.ErrorController; @@ -36,6 +37,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration.DefaultTemplateResolverConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; @@ -73,6 +75,9 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom @Value("${error.path:/error}") private String errorPath = "/error"; + @Autowired + private ServerProperties properties; + @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController() { @@ -81,7 +86,8 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom @Override public void customize(ConfigurableEmbeddedServletContainer container) { - container.addErrorPages(new ErrorPage(this.errorPath)); + container.addErrorPages(new ErrorPage(this.properties.getServletPrefix() + + this.errorPath)); } @Configuration 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 316b97e17c3..9f6ec798c6a 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.SecurityAutoConfiguration 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.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -122,6 +123,9 @@ public class ManagementSecurityAutoConfiguration { @Autowired private SecurityProperties security; + @Autowired + private ServerProperties server; + @Override public void configure(WebSecurity builder) throws Exception { } @@ -145,7 +149,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) { @@ -180,6 +185,9 @@ public class ManagementSecurityAutoConfiguration { @Autowired private ManagementServerProperties management; + @Autowired + private ServerProperties server; + @Autowired(required = false) private EndpointHandlerMapping endpointHandlerMapping; @@ -194,6 +202,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/batch/BatchAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java index 85e69b3b212..6571c0ac74c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java @@ -101,7 +101,7 @@ public class BatchAutoConfiguration { JobExplorerFactoryBean factory = new JobExplorerFactoryBean(); factory.setDataSource(dataSource); factory.afterPropertiesSet(); - return (JobExplorer) factory.getObject(); + return factory.getObject(); } @Bean 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 fd2f76934fc..8f5a51ea074 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.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -141,6 +142,9 @@ public class SpringBootWebSecurityConfiguration { @Autowired private SecurityProperties security; + @Autowired + private ServerProperties server; + @Override public void configure(WebSecurity builder) throws Exception { } @@ -149,7 +153,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 9d3d9f8f201..d81a9fa1a7b 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; @@ -265,4 +266,41 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { } + public String getServletPrefix() { + String result = this.servletPath; + if (result.contains("*")) { + result = result.substring(0, result.indexOf("*")); + } + if (result.endsWith("/")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + 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 77454d81611..d7315604b3e 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, @@ -117,7 +123,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..f0d93904d72 --- /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("ok")); + } + + @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..33f6f7af8a4 --- /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()); + } + +}