diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfiguration.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfiguration.java index 29d86560287..377e73c0f23 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfiguration.java @@ -23,10 +23,13 @@ import javax.servlet.Filter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties.Servlet; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -45,7 +48,11 @@ import org.springframework.boot.devtools.restart.server.SourceFolderUrlFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.server.ServerHttpRequest; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; /** * {@link EnableAutoConfiguration Auto-configuration} for remote development support. @@ -53,12 +60,14 @@ import org.springframework.http.server.ServerHttpRequest; * @author Phillip Webb * @author Rob Winch * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.3.0 */ @Configuration(proxyBeanMethods = false) @Conditional(OnEnabledDevToolsCondition.class) @ConditionalOnProperty(prefix = "spring.devtools.remote", name = "secret") @ConditionalOnClass({ Filter.class, ServerHttpRequest.class }) +@AutoConfigureAfter(SecurityAutoConfiguration.class) @EnableConfigurationProperties({ ServerProperties.class, DevToolsProperties.class }) public class RemoteDevToolsAutoConfiguration { @@ -127,4 +136,25 @@ public class RemoteDevToolsAutoConfiguration { } + @Configuration + @Order(SecurityProperties.BASIC_AUTH_ORDER - 1) + @ConditionalOnClass(WebSecurityConfigurerAdapter.class) + static class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final String url; + + SecurityConfiguration(DevToolsProperties devToolsProperties, ServerProperties serverProperties) { + Servlet servlet = serverProperties.getServlet(); + String servletContextPath = (servlet.getContextPath() != null) ? servlet.getContextPath() : ""; + this.url = servletContextPath + devToolsProperties.getRemote().getContextPath() + "/restart"; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.requestMatcher(new AntPathRequestMatcher(this.url)).authorizeRequests().anyRequest().anonymous().and() + .csrf().disable(); + } + + } + } diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfigurationTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfigurationTests.java index 7fc6bde73d0..2f903a02b4d 100644 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/RemoteDevToolsAutoConfigurationTests.java @@ -19,6 +19,8 @@ package org.springframework.boot.devtools.autoconfigure; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import javax.servlet.Filter; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +28,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.devtools.remote.server.DispatcherFilter; import org.springframework.boot.devtools.restart.MockRestarter; import org.springframework.boot.devtools.restart.server.HttpRestartServer; @@ -41,16 +44,22 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.security.config.BeanIds; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests for {@link RemoteDevToolsAutoConfiguration}. * * @author Rob Winch * @author Phillip Webb + * @author Madhura Bhave */ @ExtendWith(MockRestarter.class) class RemoteDevToolsAutoConfigurationTests { @@ -138,6 +147,42 @@ class RemoteDevToolsAutoConfigurationTests { assertRestartInvoked(true); } + @Test + void securityConfigurationShouldAllowAccess() throws Exception { + this.context = getContext(() -> loadContext("spring.devtools.remote.secret:supersecret")); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + Filter securityFilterChain = this.context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).addFilter(securityFilterChain) + .addFilter(filter).build(); + mockMvc.perform(MockMvcRequestBuilders.get(DEFAULT_CONTEXT_PATH + "/restart").header(DEFAULT_SECRET_HEADER_NAME, + "supersecret")).andExpect(status().isOk()); + assertRestartInvoked(true); + } + + @Test + void securityConfigurationShouldAllowAccessToCustomPath() throws Exception { + this.context = getContext(() -> loadContext("spring.devtools.remote.secret:supersecret", + "server.servlet.context-path:/test", "spring.devtools.remote.context-path:/custom")); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + Filter securityFilterChain = this.context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).addFilter(securityFilterChain) + .addFilter(filter).build(); + mockMvc.perform( + MockMvcRequestBuilders.get("/test/custom/restart").header(DEFAULT_SECRET_HEADER_NAME, "supersecret")) + .andExpect(status().isOk()); + assertRestartInvoked(true); + } + + @Test + void securityConfigurationDoesNotAffectOtherPaths() throws Exception { + this.context = getContext(() -> loadContext("spring.devtools.remote.secret:supersecret")); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + Filter securityFilterChain = this.context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).addFilter(securityFilterChain) + .addFilter(filter).build(); + mockMvc.perform(MockMvcRequestBuilders.get("/my-path")).andExpect(status().isUnauthorized()); + } + @Test void disableRestart() throws Exception { this.context = getContext(() -> loadContext("spring.devtools.remote.secret:supersecret", @@ -195,7 +240,7 @@ class RemoteDevToolsAutoConfigurationTests { } @Configuration(proxyBeanMethods = false) - @Import(RemoteDevToolsAutoConfiguration.class) + @Import({ SecurityAutoConfiguration.class, RemoteDevToolsAutoConfiguration.class }) static class Config { @Bean