From cae72ffc5ed32aa9e2c4f5d09d8706921475d044 Mon Sep 17 00:00:00 2001 From: Andrew Backes Date: Mon, 8 May 2017 11:02:09 -0700 Subject: [PATCH] feat(core): Allow configurable headers for rest endpoints (#143) * Allow configuration of arbitrary headers to rest endpoints * Allow customizable headers from a file * Allow customizable headers in the echo configuration yml --- .../spinnaker/echo/config/RestConfig.groovy | 72 +++++++++++++--- .../echo/config/RestProperties.groovy | 2 + .../echo/config/RestConfigSpec.groovy | 86 +++++++++++++++++++ 3 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy diff --git a/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestConfig.groovy b/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestConfig.groovy index ad1404d71..89425c04a 100644 --- a/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestConfig.groovy +++ b/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestConfig.groovy @@ -16,11 +16,10 @@ package com.netflix.spinnaker.echo.config -import static retrofit.Endpoints.newFixedEndpoint - -import org.apache.commons.codec.binary.Base64 import com.netflix.spinnaker.echo.rest.RestService import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import org.apache.commons.codec.binary.Base64 import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.config.ConfigurableBeanFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty @@ -34,9 +33,12 @@ import retrofit.client.Client import retrofit.client.OkClient import retrofit.converter.JacksonConverter +import static retrofit.Endpoints.newFixedEndpoint + /** * Rest endpoint configuration */ +@Slf4j @Configuration @ConditionalOnProperty('rest.enabled') @CompileStatic @@ -54,8 +56,44 @@ class RestConfig { return LogLevel.valueOf(retrofitLogLevel) } + interface RequestInterceptorAttacher { + void attach(RestAdapter.Builder builder, RequestInterceptor interceptor) + } + @Bean - RestUrls restServices(RestProperties restProperties, Client retrofitClient, LogLevel retrofitLogLevel) { + RequestInterceptorAttacher requestInterceptorAttacher() { + new RequestInterceptorAttacher() { + @Override + public void attach(RestAdapter.Builder builder, RequestInterceptor interceptor) { + builder.setRequestInterceptor(interceptor) + } + } + } + + interface HeadersFromFile { + Map headers(String path) + } + + @Bean + HeadersFromFile headersFromFile() { + new HeadersFromFile() { + Map headers(String path) { + Map headers = new HashMap<>() + new File(path).eachLine { line -> + def pair = line.split(":") + if (pair.length == 2) { + headers[pair[0]] = pair[1].trim() + } else { + log.warn("Could not parse header '$line' in '$path'") + } + } + return headers + } + } + } + + @Bean + RestUrls restServices(RestProperties restProperties, Client retrofitClient, LogLevel retrofitLogLevel, RequestInterceptorAttacher requestInterceptorAttacher, HeadersFromFile headersFromFile) { RestUrls restUrls = new RestUrls() @@ -68,16 +106,31 @@ class RestConfig { .setLogLevel(retrofitLogLevel) .setConverter(new JacksonConverter()) + Map headers = new HashMap<>() + if (endpoint.username && endpoint.password) { - RequestInterceptor authInterceptor = new RequestInterceptor() { + String auth = "Basic " + Base64.encodeBase64String("${endpoint.username}:${endpoint.password}".getBytes()) + headers["Authorization"] = auth + } + + if (endpoint.headers) { + headers += endpoint.headers + } + + if (endpoint.headersFile) { + headers += headersFromFile.headers(endpoint.headersFile) + } + + if (headers) { + RequestInterceptor headerInterceptor = new RequestInterceptor() { @Override public void intercept(RequestInterceptor.RequestFacade request) { - String auth = "Basic " + Base64.encodeBase64String("${endpoint.username}:${endpoint.password}".getBytes()) - request.addHeader("Authorization", auth) + headers.each { k, v -> + request.addHeader(k, v) + } } } - - restAdapterBuilder.setRequestInterceptor(authInterceptor) + requestInterceptorAttacher.attach(restAdapterBuilder, headerInterceptor) } restUrls.services.add( @@ -90,5 +143,4 @@ class RestConfig { restUrls } - } diff --git a/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestProperties.groovy b/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestProperties.groovy index 0de2aa35f..9caba565c 100644 --- a/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestProperties.groovy +++ b/echo-rest/src/main/groovy/com/netflix/spinnaker/echo/config/RestProperties.groovy @@ -41,6 +41,8 @@ class RestProperties { String url String username String password + Map headers + String headersFile Boolean flatten = false } diff --git a/echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy b/echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy new file mode 100644 index 000000000..b28570b79 --- /dev/null +++ b/echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy @@ -0,0 +1,86 @@ +package com.netflix.spinnaker.echo.config + +import retrofit.RequestInterceptor +import retrofit.RestAdapter +import spock.lang.Specification +import spock.lang.Subject + +class RestConfigSpec extends Specification { + + @Subject + config = new RestConfig() + + def request = Mock(RequestInterceptor.RequestFacade) + def EmptyHeadersFile = Mock(RestConfig.HeadersFromFile) + def attacher = new RestConfig.RequestInterceptorAttacher() { + RequestInterceptor interceptor + @Override + public void attach(RestAdapter.Builder builder, RequestInterceptor interceptor) { + this.interceptor = interceptor + } + } + + void configureRestServices(RestProperties.RestEndpointConfiguration endpoint, RestConfig.HeadersFromFile headersFromFile) { + RestProperties restProperties = new RestProperties(endpoints: [endpoint]) + config.restServices(restProperties, config.retrofitClient(), config.retrofitLogLevel("BASIC"), attacher, headersFromFile) + } + + void "Generate basic auth header"() { + given: + RestProperties.RestEndpointConfiguration endpoint = new RestProperties.RestEndpointConfiguration( + url: "http://localhost:9090", + username: "testuser", + password: "testpassword") + configureRestServices(endpoint, EmptyHeadersFile) + + when: + attacher.interceptor.intercept(request) + + then: + 1 * request.addHeader("Authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk") + 0 * request.addHeader(_, _) + } + + void "'Authorization' header over generated basic auth header"() { + given: + RestProperties.RestEndpointConfiguration endpoint = new RestProperties.RestEndpointConfiguration( + url: "http://localhost:9090", + username: "testuser", + password: "testpassword", + headers: ["Authorization": "FromConfig"]) + configureRestServices(endpoint, EmptyHeadersFile) + + when: + attacher.interceptor.intercept(request) + + then: + 1 * request.addHeader("Authorization", "FromConfig") + 0 * request.addHeader(_, _) + } + + void "'Authorization' headerFile over all others"() { + given: + RestProperties.RestEndpointConfiguration endpoint = new RestProperties.RestEndpointConfiguration( + url: "http://localhost:9090", + username: "testuser", + password: "testpassword", + headers: ["Authorization": "FromConfig"], + headersFile: "/testfile") + RestConfig.HeadersFromFile headersFromFile = new RestConfig.HeadersFromFile() { + @Override + Map headers(String path) { + return [ + "Authorization": "FromFile" + ] + } + } + configureRestServices(endpoint, headersFromFile) + + when: + attacher.interceptor.intercept(request) + + then: + 1 * request.addHeader("Authorization", "FromFile") + 0 * request.addHeader(_, _) + } +}