From 47aa19fa87f4878022a7568a1ee6d4a77b57e6a2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 Dec 2016 22:47:43 +0100 Subject: [PATCH] Use Spring Vault for VaultEnvironmentRepository (#587) VaultEnvironmentRepository now uses Spring Vault and VaultClient to access Vault. Using VaultEnvironmentRepository requires the org.springprojects.vault:spring-vault-core dependency. Fixes gh-586. --- spring-cloud-config-dependencies/pom.xml | 5 + spring-cloud-config-server/pom.xml | 5 + .../VaultEnvironmentRepository.java | 159 ++++++++---------- .../VaultEnvironmentRepositoryTests.java | 32 +++- 4 files changed, 105 insertions(+), 96 deletions(-) diff --git a/spring-cloud-config-dependencies/pom.xml b/spring-cloud-config-dependencies/pom.xml index 2c128d4e..6e038a8f 100644 --- a/spring-cloud-config-dependencies/pom.xml +++ b/spring-cloud-config-dependencies/pom.xml @@ -35,6 +35,11 @@ spring-cloud-config-monitor ${project.version} + + org.springframework.vault + spring-vault-core + 1.0.0.M1 + org.eclipse.jgit org.eclipse.jgit diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml index 7c4454e7..311e1bb7 100644 --- a/spring-cloud-config-server/pom.xml +++ b/spring-cloud-config-server/pom.xml @@ -42,6 +42,11 @@ org.springframework.security spring-security-rsa + + org.springframework.vault + spring-vault-core + true + org.eclipse.jgit org.eclipse.jgit diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java index e7e5290a..ed5d553b 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java @@ -1,43 +1,61 @@ +/* + * Copyright 2013-2016 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.cloud.config.server.environment; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import javax.servlet.http.HttpServletRequest; -import com.fasterxml.jackson.annotation.JsonRawValue; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.JsonNode; import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.Range; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.PropertySource; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; -import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.vault.client.VaultClient; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultException; +import org.springframework.vault.client.VaultResponseEntity; +import org.springframework.vault.support.VaultResponseSupport; +import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestTemplate; -import com.fasterxml.jackson.annotation.JsonProperty; - import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER; import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER; /** * @author Spencer Gibb + * @author Mark Paluch */ @ConfigurationProperties("spring.cloud.config.server.vault") -public class VaultEnvironmentRepository implements EnvironmentRepository { - - public static final String VAULT_TOKEN = "X-Vault-Token"; +public class VaultEnvironmentRepository implements EnvironmentRepository, InitializingBean { /** Vault host. Defaults to 127.0.0.1. */ @NotEmpty @@ -67,6 +85,7 @@ public class VaultEnvironmentRepository implements EnvironmentRepository { private HttpServletRequest request; private EnvironmentWatch watch; + private VaultClient client; public VaultEnvironmentRepository(HttpServletRequest request, EnvironmentWatch watch, RestTemplate rest) { this.request = request; @@ -74,6 +93,11 @@ public class VaultEnvironmentRepository implements EnvironmentRepository { this.rest = rest; } + @Override + public void afterPropertiesSet() { + this.client = new VaultClient(rest, getVaultEndpoint()); + } + @Override public Environment findOne(String application, String profile, String label) { @@ -136,34 +160,47 @@ public class VaultEnvironmentRepository implements EnvironmentRepository { } String read(String key) { - String url = String.format("%s://%s:%s/v1/{backend}/{key}", this.scheme, - this.host, this.port); - - HttpHeaders headers = new HttpHeaders(); String token = request.getHeader(TOKEN_HEADER); if (!StringUtils.hasLength(token)) { - throw new IllegalArgumentException("Missing required header: "+TOKEN_HEADER); - } - headers.add(VAULT_TOKEN, token); - try { - ResponseEntity response = this.rest.exchange(url, - HttpMethod.GET, new HttpEntity<>(headers), VaultResponse.class, - this.backend, key); - - HttpStatus status = response.getStatusCode(); - if (status == HttpStatus.OK) { - return response.getBody().getData(); - } - } - catch (HttpStatusCodeException e) { - if (e.getStatusCode() == HttpStatus.NOT_FOUND) { - return null; - } - throw e; + throw new IllegalArgumentException("Missing required header: " + TOKEN_HEADER); } - return null; + VaultToken vaultToken = VaultToken.of(token); + VaultResponseEntity response = client.exchange("{backend}/{key}", HttpMethod.GET, + new HttpEntity<>(VaultClient.createHeaders(vaultToken)), VaultResponse.class, getUriVariables(key)); + + HttpStatus status = response.getStatusCode(); + + if (status == HttpStatus.OK) { + + JsonNode data = response.getBody().getData(); + return data != null ? data.toString() : null; + } + + if (status == HttpStatus.NOT_FOUND) { + return null; + } + + throw new VaultException(response.getMessage()); + } + + private Map getUriVariables(String key) { + Map uriVariables = new HashMap<>(2, 1); + uriVariables.put("backend", backend); + uriVariables.put("key", key); + return uriVariables; + } + + private VaultEndpoint getVaultEndpoint() { + + VaultEndpoint vaultEndpoint = new VaultEndpoint(); + + vaultEndpoint.setScheme(scheme); + vaultEndpoint.setHost(host); + vaultEndpoint.setPort(port); + + return vaultEndpoint; } public void setHost(String host) { @@ -190,61 +227,7 @@ public class VaultEnvironmentRepository implements EnvironmentRepository { this.profileSeparator = profileSeparator; } - static class VaultResponse { - private String auth; - - private Object data; - - @JsonProperty("lease_duration") - private long leaseDuration; - - @JsonProperty("lease_id") - private String leaseId; - - private boolean renewable; - - public VaultResponse() { - } - - public String getAuth() { - return auth; - } - - public void setAuth(String auth) { - this.auth = auth; - } - - @JsonRawValue - public String getData() { - return data == null ? null : data.toString(); - } - - public void setData(JsonNode data) { - this.data = data; - } - - public long getLeaseDuration() { - return leaseDuration; - } - - public void setLeaseDuration(long leaseDuration) { - this.leaseDuration = leaseDuration; - } - - public String getLeaseId() { - return leaseId; - } - - public void setLeaseId(String leaseId) { - this.leaseId = leaseId; - } - - public boolean isRenewable() { - return renewable; - } - - public void setRenewable(boolean renewable) { - this.renewable = renewable; - } + @JsonIgnoreProperties(ignoreUnknown = true) + static class VaultResponse extends VaultResponseSupport { } } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java index a3962d85..9db71148 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java @@ -3,9 +3,12 @@ package org.springframework.cloud.config.server.environment; import static org.junit.Assert.assertEquals; import java.io.IOException; +import java.net.URI; import java.util.HashMap; import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -16,13 +19,16 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriTemplateHandler; /** * @author Spencer Gibb * @author Ryan Baxter + * @author Mark Paluch */ public class VaultEnvironmentRepositoryTests { + private ObjectMapper objectMapper = new ObjectMapper(); @Before public void init() {} @@ -33,22 +39,22 @@ public class VaultEnvironmentRepositoryTests { configRequest.addHeader("X-CONFIG-TOKEN", "mytoken"); RestTemplate rest = Mockito.mock(RestTemplate.class); ResponseEntity myAppResp = Mockito.mock(ResponseEntity.class); + Mockito.when(rest.getUriTemplateHandler()).thenReturn(new DefaultUriTemplateHandler()); Mockito.when(myAppResp.getStatusCode()).thenReturn(HttpStatus.OK); VaultEnvironmentRepository.VaultResponse myAppVaultResp = Mockito.mock(VaultEnvironmentRepository.VaultResponse.class); - Mockito.when(myAppVaultResp.getData()).thenReturn("{\"foo\":\"bar\"}"); + Mockito.when(myAppVaultResp.getData()).thenReturn(asJsonNode("{\"foo\":\"bar\"}")); Mockito.when(myAppResp.getBody()).thenReturn(myAppVaultResp); - Mockito.when(rest.exchange(Mockito.eq("http://127.0.0.1:8200/v1/{backend}/{key}"), - Mockito.eq(HttpMethod.GET), Mockito.any(HttpEntity.class), Mockito.eq(VaultEnvironmentRepository.VaultResponse.class), - Mockito.eq("secret"), Mockito.eq("myapp"))).thenReturn(myAppResp); + Mockito.when(rest.exchange(Mockito.eq(URI.create("http://127.0.0.1:8200/v1/secret/myapp")), + Mockito.eq(HttpMethod.GET), Mockito.any(HttpEntity.class), Mockito.eq(VaultEnvironmentRepository.VaultResponse.class))).thenReturn(myAppResp); ResponseEntity appResp = Mockito.mock(ResponseEntity.class); Mockito.when(appResp.getStatusCode()).thenReturn(HttpStatus.OK); VaultEnvironmentRepository.VaultResponse appVaultResp = Mockito.mock(VaultEnvironmentRepository.VaultResponse.class); Mockito.when(appVaultResp.getData()).thenReturn(null); Mockito.when(appResp.getBody()).thenReturn(appVaultResp); - Mockito.when(rest.exchange(Mockito.eq("http://127.0.0.1:8200/v1/{backend}/{key}"), - Mockito.eq(HttpMethod.GET), Mockito.any(HttpEntity.class), Mockito.eq(VaultEnvironmentRepository.VaultResponse.class), - Mockito.eq("secret"), Mockito.eq("application"))).thenReturn(appResp); + Mockito.when(rest.exchange(Mockito.eq(URI.create("http://127.0.0.1:8200/v1/secret/application")), + Mockito.eq(HttpMethod.GET), Mockito.any(HttpEntity.class), Mockito.eq(VaultEnvironmentRepository.VaultResponse.class))).thenReturn(appResp); VaultEnvironmentRepository repo = new VaultEnvironmentRepository(configRequest, new EnvironmentWatch.Default(), rest); + repo.afterPropertiesSet(); Environment e = repo.findOne("myapp", null, null); assertEquals("myapp", e.getName()); Map result = new HashMap(); @@ -61,14 +67,24 @@ public class VaultEnvironmentRepositoryTests { MockHttpServletRequest configRequest = new MockHttpServletRequest(); RestTemplate rest = Mockito.mock(RestTemplate.class); ResponseEntity myAppResp = Mockito.mock(ResponseEntity.class); + Mockito.when(rest.getUriTemplateHandler()).thenReturn(new DefaultUriTemplateHandler()); Mockito.when(myAppResp.getStatusCode()).thenReturn(HttpStatus.OK); VaultEnvironmentRepository.VaultResponse myAppVaultResp = Mockito.mock(VaultEnvironmentRepository.VaultResponse.class); - Mockito.when(myAppVaultResp.getData()).thenReturn("{\"foo\":\"bar\"}"); + Mockito.when(myAppVaultResp.getData()).thenReturn(asJsonNode("{\"foo\":\"bar\"}")); Mockito.when(myAppResp.getBody()).thenReturn(myAppVaultResp); Mockito.when(rest.exchange(Mockito.eq("http://127.0.0.1:8200/v1/{backend}/{key}"), Mockito.eq(HttpMethod.GET), Mockito.any(HttpEntity.class), Mockito.eq(VaultEnvironmentRepository.VaultResponse.class), Mockito.eq("secret"), Mockito.eq("myapp"))).thenReturn(myAppResp); VaultEnvironmentRepository repo = new VaultEnvironmentRepository(configRequest, new EnvironmentWatch.Default(), rest); + repo.afterPropertiesSet(); repo.findOne("myapp", null, null); } + + private JsonNode asJsonNode(String content) { + try { + return objectMapper.readTree(content); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } }