From 207347e1508555f0f4e47607b09a64a241120f17 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 1 Jun 2015 13:24:18 -0700 Subject: [PATCH] Add header based remote access security Update the remote endpoints to use 'shared secret' authentication. Secrets are provided as Environment properties and transfered using a custom HTTP header. See gh-3082 --- ...RemoteDeveloperToolsAutoConfiguration.java | 15 ++- .../RemoteDeveloperToolsProperties.java | 29 +++++ .../remote/client/HttpHeaderInterceptor.java | 60 +++++++++ .../client/RemoteClientConfiguration.java | 20 ++- .../server/HttpHeaderAccessManager.java | 48 ++++++++ ...eDeveloperToolsAutoConfigurationTests.java | 55 ++++++++- .../client/HttpHeaderInterceptorTests.java | 115 ++++++++++++++++++ .../RemoteClientConfigurationTests.java | 20 ++- .../server/HttpHeaderAccessManagerTests.java | 109 +++++++++++++++++ 9 files changed, 457 insertions(+), 14 deletions(-) create mode 100644 spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptor.java create mode 100644 spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManager.java create mode 100644 spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptorTests.java create mode 100644 spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManagerTests.java diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java index dfb2d3952a..1a5239ddf8 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfiguration.java @@ -34,6 +34,7 @@ import org.springframework.boot.developertools.remote.server.Dispatcher; import org.springframework.boot.developertools.remote.server.DispatcherFilter; import org.springframework.boot.developertools.remote.server.Handler; import org.springframework.boot.developertools.remote.server.HandlerMapper; +import org.springframework.boot.developertools.remote.server.HttpHeaderAccessManager; import org.springframework.boot.developertools.remote.server.HttpStatusHandler; import org.springframework.boot.developertools.remote.server.UrlHandlerMapper; import org.springframework.boot.developertools.restart.server.DefaultSourceFolderUrlFilter; @@ -56,7 +57,7 @@ import org.springframework.http.server.ServerHttpRequest; * @since 1.3.0 */ @Configuration -@ConditionalOnProperty(prefix = "spring.developertools.remote", name = "enabled") +@ConditionalOnProperty(prefix = "spring.developertools.remote", name = "secret") @ConditionalOnClass({ Filter.class, ServerHttpRequest.class }) @EnableConfigurationProperties(DeveloperToolsProperties.class) public class RemoteDeveloperToolsAutoConfiguration { @@ -67,6 +68,14 @@ public class RemoteDeveloperToolsAutoConfiguration { @Autowired private DeveloperToolsProperties properties; + @Bean + @ConditionalOnMissingBean + public AccessManager remoteDeveloperToolsAccessManager() { + RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote(); + return new HttpHeaderAccessManager(remoteProperties.getSecretHeaderName(), + remoteProperties.getSecret()); + } + @Bean public HandlerMapper remoteDeveloperToolsHealthCheckHandlerMapper() { Handler handler = new HttpStatusHandler(); @@ -76,8 +85,8 @@ public class RemoteDeveloperToolsAutoConfiguration { @Bean @ConditionalOnMissingBean public DispatcherFilter remoteDeveloperToolsDispatcherFilter( - Collection mappers) { - Dispatcher dispatcher = new Dispatcher(AccessManager.PERMIT_ALL, mappers); + AccessManager accessManager, Collection mappers) { + Dispatcher dispatcher = new Dispatcher(accessManager, mappers); return new DispatcherFilter(dispatcher); } diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java index eadd0ba47e..a975dcb1af 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsProperties.java @@ -28,11 +28,24 @@ public class RemoteDeveloperToolsProperties { public static final String DEFAULT_CONTEXT_PATH = "/.~~spring-boot!~"; + public static final String DEFAULT_SECRET_HEADER_NAME = "X-AUTH-TOKEN"; + /** * Context path used to handle the remote connection. */ private String contextPath = DEFAULT_CONTEXT_PATH; + /** + * A shared secret required to establish a connection (required to enable remote + * support). + */ + private String secret; + + /** + * HTTP header used to transfer the shared secret. + */ + private String secretHeaderName = DEFAULT_SECRET_HEADER_NAME; + private Restart restart = new Restart(); private Debug debug = new Debug(); @@ -45,6 +58,22 @@ public class RemoteDeveloperToolsProperties { this.contextPath = contextPath; } + public String getSecret() { + return this.secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public String getSecretHeaderName() { + return this.secretHeaderName; + } + + public void setSecretHeaderName(String secretHeaderName) { + this.secretHeaderName = secretHeaderName; + } + public Restart getRestart() { return this.restart; } diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptor.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptor.java new file mode 100644 index 0000000000..b6effd43bc --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2015 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.developertools.remote.client; + +import java.io.IOException; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; + +/** + * {@link ClientHttpRequestInterceptor} to populate arbitrary HTTP headers with a value. + * For example, it might be used to provide an X-AUTH-TOKEN and value for security + * purposes. + * + * @author Rob Winch + * @since 1.3.0 + */ +public class HttpHeaderInterceptor implements ClientHttpRequestInterceptor { + + private final String name; + + private final String value; + + /** + * Creates a new {@link HttpHeaderInterceptor} instance. + * @param name the header name to populate. Cannot be null or empty. + * @param value the header value to populate. Cannot be null or empty. + */ + public HttpHeaderInterceptor(String name, String value) { + Assert.hasLength(name, "Name must not be empty"); + Assert.hasLength(value, "Value" + " must not be empty"); + this.name = name; + this.value = value; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + request.getHeaders().add(this.name, this.value); + return execution.execute(request, body); + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java index 27da0482b6..432686dfeb 100644 --- a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/client/RemoteClientConfiguration.java @@ -17,6 +17,8 @@ package org.springframework.boot.developertools.remote.client; import java.net.URL; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -51,7 +53,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.InterceptingClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.util.Assert; /** * Configuration used to connect to remote Spring Boot applications. @@ -79,7 +84,20 @@ public class RemoteClientConfiguration { @Bean public ClientHttpRequestFactory clientHttpRequestFactory() { - return new SimpleClientHttpRequestFactory(); + List interceptors = Arrays + .asList(getSecurityInterceptor()); + return new InterceptingClientHttpRequestFactory( + new SimpleClientHttpRequestFactory(), interceptors); + } + + private ClientHttpRequestInterceptor getSecurityInterceptor() { + RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote(); + String secretHeaderName = remoteProperties.getSecretHeaderName(); + String secret = remoteProperties.getSecret(); + Assert.state(secret != null, + "The environment value 'spring.developertools.remote.secret' " + + "is required to secure your connection."); + return new HttpHeaderInterceptor(secretHeaderName, secret); } @PostConstruct diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManager.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManager.java new file mode 100644 index 0000000000..e5ab6f61f8 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManager.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2015 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.developertools.remote.server; + +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.util.Assert; + +/** + * {@link AccessManager} that checks for the presence of a HTTP header secret. + * + * @author Rob Winch + * @author Phillip Webb + * @since 1.3.0 + */ +public class HttpHeaderAccessManager implements AccessManager { + + private final String headerName; + + private final String expectedSecret; + + public HttpHeaderAccessManager(String headerName, String expectedSecret) { + Assert.hasLength(headerName, "HeaderName must not be empty"); + Assert.hasLength(expectedSecret, "ExpectedSecret must not be empty"); + this.headerName = headerName; + this.expectedSecret = expectedSecret; + } + + @Override + public boolean isAllowed(ServerHttpRequest request) { + String providedSecret = request.getHeaders().getFirst(this.headerName); + return this.expectedSecret.equals(providedSecret); + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java index a56b797a09..920dbafff0 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/autoconfigure/RemoteDeveloperToolsAutoConfigurationTests.java @@ -60,6 +60,8 @@ public class RemoteDeveloperToolsAutoConfigurationTests { private static final String DEFAULT_CONTEXT_PATH = RemoteDeveloperToolsProperties.DEFAULT_CONTEXT_PATH; + private static final String DEFAULT_SECRET_HEADER_NAME = RemoteDeveloperToolsProperties.DEFAULT_SECRET_HEADER_NAME; + @Rule public MockRestarter mockRestarter = new MockRestarter(); @@ -88,27 +90,55 @@ public class RemoteDeveloperToolsAutoConfigurationTests { } } + @Test + public void disabledIfRemoteSecretIsMissing() throws Exception { + loadContext("a:b"); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(DispatcherFilter.class); + } + @Test public void ignoresUnmappedUrl() throws Exception { - loadContext("spring.developertools.remote.enabled:true"); + loadContext("spring.developertools.remote.secret:supersecret"); DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); this.request.setRequestURI("/restart"); + this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret"); + filter.doFilter(this.request, this.response, this.chain); + assertRestartInvoked(false); + } + + @Test + public void ignoresIfMissingSecretFromRequest() throws Exception { + loadContext("spring.developertools.remote.secret:supersecret"); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart"); + filter.doFilter(this.request, this.response, this.chain); + assertRestartInvoked(false); + } + + @Test + public void ignoresInvalidSecretInRequest() throws Exception { + loadContext("spring.developertools.remote.secret:supersecret"); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart"); + this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "invalid"); filter.doFilter(this.request, this.response, this.chain); assertRestartInvoked(false); } @Test public void invokeRestartWithDefaultSetup() throws Exception { - loadContext("spring.developertools.remote.enabled:true"); + loadContext("spring.developertools.remote.secret:supersecret"); DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart"); + this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret"); filter.doFilter(this.request, this.response, this.chain); assertRestartInvoked(true); } @Test public void disableRestart() throws Exception { - loadContext("spring.developertools.remote.enabled:true", + loadContext("spring.developertools.remote.secret:supersecret", "spring.developertools.remote.restart.enabled:false"); this.thrown.expect(NoSuchBeanDefinitionException.class); this.context.getBean("remoteRestartHanderMapper"); @@ -116,16 +146,28 @@ public class RemoteDeveloperToolsAutoConfigurationTests { @Test public void invokeTunnelWithDefaultSetup() throws Exception { - loadContext("spring.developertools.remote.enabled:true"); + loadContext("spring.developertools.remote.secret:supersecret"); + DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); + this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug"); + this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret"); + filter.doFilter(this.request, this.response, this.chain); + assertTunnelInvoked(true); + } + + @Test + public void invokeTunnelWithCustomHeaderName() throws Exception { + loadContext("spring.developertools.remote.secret:supersecret", + "spring.developertools.remote.secretHeaderName:customheader"); DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug"); + this.request.addHeader("customheader", "supersecret"); filter.doFilter(this.request, this.response, this.chain); assertTunnelInvoked(true); } @Test public void disableRemoteDebug() throws Exception { - loadContext("spring.developertools.remote.enabled:true", + loadContext("spring.developertools.remote.secret:supersecret", "spring.developertools.remote.debug.enabled:false"); this.thrown.expect(NoSuchBeanDefinitionException.class); this.context.getBean("remoteDebugHanderMapper"); @@ -133,9 +175,10 @@ public class RemoteDeveloperToolsAutoConfigurationTests { @Test public void developerToolsHealthReturns200() throws Exception { - loadContext("spring.developertools.remote.enabled:true"); + loadContext("spring.developertools.remote.secret:supersecret"); DispatcherFilter filter = this.context.getBean(DispatcherFilter.class); this.request.setRequestURI(DEFAULT_CONTEXT_PATH); + this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret"); this.response.setStatus(500); filter.doFilter(this.request, this.response, this.chain); assertThat(this.response.getStatus(), equalTo(200)); diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptorTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptorTests.java new file mode 100644 index 0000000000..c23665afc1 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/HttpHeaderInterceptorTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2015 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.developertools.remote.client; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Tests for {@link HttpHeaderInterceptor}. + * + * @author Rob Winch + * @since 1.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class HttpHeaderInterceptorTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private String name; + + private String value; + + private HttpHeaderInterceptor interceptor; + + private HttpRequest request; + + private byte[] body; + + @Mock + private ClientHttpRequestExecution execution; + + @Mock + private ClientHttpResponse response; + + private MockHttpServletRequest httpRequest; + + @Before + public void setup() throws IOException { + this.body = new byte[] {}; + this.httpRequest = new MockHttpServletRequest(); + this.request = new ServletServerHttpRequest(this.httpRequest); + this.name = "X-AUTH-TOKEN"; + this.value = "secret"; + given(this.execution.execute(this.request, this.body)).willReturn(this.response); + this.interceptor = new HttpHeaderInterceptor(this.name, this.value); + } + + @Test + public void constructorNullHeaderName() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be empty"); + new HttpHeaderInterceptor(null, this.value); + } + + @Test + public void constructorEmptyHeaderName() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Name must not be empty"); + new HttpHeaderInterceptor("", this.value); + } + + @Test + public void constructorNullHeaderValue() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Value must not be empty"); + new HttpHeaderInterceptor(this.name, null); + } + + @Test + public void constructorEmptyHeaderValue() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Value must not be empty"); + new HttpHeaderInterceptor(this.name, ""); + } + + @Test + public void intercept() throws IOException { + ClientHttpResponse result = this.interceptor.intercept(this.request, this.body, + this.execution); + assertThat(this.request.getHeaders().getFirst(this.name), equalTo(this.value)); + assertThat(result, equalTo(this.response)); + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java index 5aad2cc81a..ccfb2b0f88 100644 --- a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/client/RemoteClientConfigurationTests.java @@ -25,6 +25,7 @@ import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; @@ -92,16 +93,23 @@ public class RemoteClientConfigurationTests { @Test public void warnIfNotHttps() throws Exception { - configureWithRemoteUrl("http://localhost"); + configure("http://localhost", true); assertThat(this.output.toString(), containsString("is insecure")); } @Test public void doesntWarnIfUsingHttps() throws Exception { - configureWithRemoteUrl("https://localhost"); + configure("https://localhost", true); assertThat(this.output.toString(), not(containsString("is insecure"))); } + @Test + public void failIfNoSecret() throws Exception { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("required to secure your connection"); + configure("http://localhost", false); + } + @Test public void liveReloadOnClassPathChanged() throws Exception { configure(); @@ -138,10 +146,10 @@ public class RemoteClientConfigurationTests { } private void configure(String... pairs) { - configureWithRemoteUrl("http://localhost", pairs); + configure("http://localhost", true, pairs); } - private void configureWithRemoteUrl(String remoteUrl, String... pairs) { + private void configure(String remoteUrl, boolean setSecret, String... pairs) { this.context = new AnnotationConfigEmbeddedWebApplicationContext(); new RestartScopeInitializer().initialize(this.context); this.context.register(Config.class, RemoteClientConfiguration.class); @@ -149,6 +157,10 @@ public class RemoteClientConfigurationTests { + RemoteClientConfigurationTests.remotePort; EnvironmentTestUtils.addEnvironment(this.context, remoteUrlProperty); EnvironmentTestUtils.addEnvironment(this.context, pairs); + if (setSecret) { + EnvironmentTestUtils.addEnvironment(this.context, + "spring.developertools.remote.secret:secret"); + } this.context.refresh(); } diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManagerTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManagerTests.java new file mode 100644 index 0000000000..637f5e10f3 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/remote/server/HttpHeaderAccessManagerTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2015 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.developertools.remote.server; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link HttpHeaderAccessManager}. + * + * @author Rob Winch + * @author Phillip Webb + */ +public class HttpHeaderAccessManagerTests { + + private static final String HEADER = "X-AUTH_TOKEN"; + + private static final String SECRET = "password"; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private MockHttpServletRequest request; + + private ServerHttpRequest serverRequest; + + private HttpHeaderAccessManager manager; + + @Before + public void setup() { + this.request = new MockHttpServletRequest("GET", "/"); + this.serverRequest = new ServletServerHttpRequest(this.request); + this.manager = new HttpHeaderAccessManager(HEADER, SECRET); + } + + @Test + public void headerNameMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("HeaderName must not be empty"); + new HttpHeaderAccessManager(null, SECRET); + } + + @Test + public void headerNameMustNotBeEmpty() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("HeaderName must not be empty"); + new HttpHeaderAccessManager("", SECRET); + } + + @Test + public void expectedSecretMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ExpectedSecret must not be empty"); + new HttpHeaderAccessManager(HEADER, null); + } + + @Test + public void expectedSecretMustNotBeEmpty() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("ExpectedSecret must not be empty"); + new HttpHeaderAccessManager(HEADER, ""); + } + + @Test + public void allowsMatching() throws Exception { + this.request.addHeader(HEADER, SECRET); + assertThat(this.manager.isAllowed(this.serverRequest), equalTo(true)); + } + + @Test + public void disallowsWrongSecret() throws Exception { + this.request.addHeader(HEADER, "wrong"); + assertThat(this.manager.isAllowed(this.serverRequest), equalTo(false)); + } + + @Test + public void disallowsNoSecret() throws Exception { + assertThat(this.manager.isAllowed(this.serverRequest), equalTo(false)); + } + + @Test + public void disallowsWrongHeader() throws Exception { + this.request.addHeader("X-WRONG", SECRET); + assertThat(this.manager.isAllowed(this.serverRequest), equalTo(false)); + } + +}