diff --git a/pom.xml b/pom.xml
index 157aa223..bfd59fdd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,12 @@
spring-boot-starter-test
test
+
+ org.assertj
+ assertj-core
+ 3.3.0
+ test
+
diff --git a/src/main/java/org/springframework/cloud/vault/AppIdUserIdMechanism.java b/src/main/java/org/springframework/cloud/vault/AppIdUserIdMechanism.java
new file mode 100644
index 00000000..ac3fd2f7
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/vault/AppIdUserIdMechanism.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 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.vault;
+
+/**
+ * @author Mark Paluch
+ */
+public interface AppIdUserIdMechanism {
+
+ /**
+ * Creates a UserId for AppId authentication.
+ *
+ * @return
+ */
+ String createUserId();
+
+}
diff --git a/src/main/java/org/springframework/cloud/vault/IpAddressUserId.java b/src/main/java/org/springframework/cloud/vault/IpAddressUserId.java
new file mode 100644
index 00000000..5b30edda
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/vault/IpAddressUserId.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 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.vault;
+
+import java.io.IOException;
+import java.net.InetAddress;
+
+/**
+ * Mechanism to generate a SHA-256 hashed and hex-encoded representation of the IP address. Can be calculated with
+ * {@code echo -n 192.168.99.1 | sha256sum}.
+ *
+ * @author Mark Paluch
+ */
+public class IpAddressUserId implements AppIdUserIdMechanism {
+
+ @Override
+ public String createUserId() {
+ try {
+ return Sha256.toSha256(InetAddress.getLocalHost().getHostAddress());
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/vault/MacAddressUserId.java b/src/main/java/org/springframework/cloud/vault/MacAddressUserId.java
new file mode 100644
index 00000000..238d052d
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/vault/MacAddressUserId.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 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.vault;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Collections;
+import java.util.List;
+
+import lombok.RequiredArgsConstructor;
+import lombok.Value;
+import lombok.extern.apachecommons.CommonsLog;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Mechanism to generate a UserId based on the Mac address. {@link MacAddressUserId} creates a hex-encoded
+ * representation of the Mac address without any separators (0123456789AB). A
+ * {@link org.springframework.cloud.vault.VaultProperties.AppIdProperties#networkInterface} can be
+ * specified optionally to select a network interface (index/name).
+ *
+ * @author Mark Paluch
+ */
+@Value
+@RequiredArgsConstructor
+@CommonsLog
+public class MacAddressUserId implements AppIdUserIdMechanism {
+
+ private final VaultProperties vaultProperties;
+
+ @Override
+ public String createUserId() {
+ try {
+
+ NetworkInterface networkInterface = null;
+ List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+
+ VaultProperties.AppIdProperties appId = vaultProperties.getAppId();
+ if (StringUtils.hasText(appId.getNetworkInterface())) {
+ try {
+ networkInterface = getNetworkInterface(Integer.parseInt(appId.getNetworkInterface()), interfaces);
+ } catch (NumberFormatException e) {
+ networkInterface = getNetworkInterface((appId.getNetworkInterface()), interfaces);
+ }
+ }
+
+ if (networkInterface == null) {
+ if (StringUtils.hasText(appId.getNetworkInterface())) {
+ log.warn(
+ String.format("Did not find a NetworkInterface applying hint %s", appId.getNetworkInterface()));
+ }
+
+ InetAddress localHost = InetAddress.getLocalHost();
+ networkInterface = NetworkInterface.getByInetAddress(localHost);
+
+ if (networkInterface == null) {
+ throw new IllegalStateException(String.format("Cannot determine NetworkInterface for %s", localHost));
+ }
+ }
+
+ byte[] mac = networkInterface.getHardwareAddress();
+ if (mac == null) {
+ throw new IllegalStateException(String.format("Network interface %s has no hardware address", networkInterface.getName()));
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mac.length; i++) {
+ sb.append(String.format("%02X", mac[i]));
+ }
+ return Sha256.toSha256(sb.toString());
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private NetworkInterface getNetworkInterface(Number hint, List interfaces) {
+
+ if (interfaces.size() > hint.intValue() && hint.intValue() >= 0) {
+ return interfaces.get(hint.intValue());
+ }
+
+ return null;
+ }
+
+ private NetworkInterface getNetworkInterface(String hint, List interfaces) {
+
+ for (NetworkInterface anInterface : interfaces) {
+ if (hint.equals(anInterface.getDisplayName()) || hint.equals(anInterface.getName())) {
+ return anInterface;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/vault/Sha256.java b/src/main/java/org/springframework/cloud/vault/Sha256.java
new file mode 100644
index 00000000..1dcc6758
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/vault/Sha256.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 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.vault;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.springframework.security.crypto.codec.Hex;
+import org.springframework.util.Assert;
+
+/**
+ * Utility to generate SHA 256 checksums.
+ *
+ * @author Mark Paluch
+ */
+class Sha256 {
+
+ /**
+ * Generates a hex-encoded SHA256 checksum from the supplied {@code content}.
+ *
+ * @param content
+ * @return
+ */
+ public static String toSha256(String content) {
+
+ Assert.hasText(content, "Content must not be empty!");
+ MessageDigest messageDigest = getMessageDigest("SHA-256");
+ byte[] digest = messageDigest.digest(content.getBytes(StandardCharsets.US_ASCII));
+ return new String(Hex.encode(digest));
+ }
+
+ /**
+ * Get a MessageDigest instance for the given algorithm. Throws an IllegalArgumentException if algorithm is
+ * unknown
+ *
+ * @return MessageDigest instance
+ * @throws IllegalArgumentException if NoSuchAlgorithmException is thrown
+ */
+ private static MessageDigest getMessageDigest(String algorithm) throws IllegalArgumentException {
+ try {
+ return MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("No such algorithm [" + algorithm + "]");
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/vault/StaticUserId.java b/src/main/java/org/springframework/cloud/vault/StaticUserId.java
new file mode 100644
index 00000000..8b375252
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/vault/StaticUserId.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 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.vault;
+
+import lombok.RequiredArgsConstructor;
+import lombok.Value;
+import lombok.extern.apachecommons.CommonsLog;
+
+/**
+ * A static UserId.
+ * @author Mark Paluch
+ */
+@Value
+@RequiredArgsConstructor
+@CommonsLog
+public class StaticUserId implements AppIdUserIdMechanism {
+
+ private final VaultProperties vaultProperties;
+
+ @Override
+ public String createUserId() {
+ return vaultProperties.getAppId().getUserId();
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java b/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java
index 1749087e..da21f59c 100644
--- a/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java
+++ b/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java
@@ -16,13 +16,19 @@
package org.springframework.cloud.vault;
+import java.util.Map;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.util.Assert;
/**
* @author Spencer Gibb
+ * @author Mark Paluch
*/
@Configuration
@EnableConfigurationProperties
@@ -30,8 +36,16 @@ import org.springframework.context.annotation.Configuration;
public class VaultBootstrapConfiguration {
@Bean
- public VaultClient vaultClient() {
- return new VaultClient(vaultProperties());
+ public VaultClient vaultClient(ApplicationContext applicationContext) {
+
+ VaultClient vaultClient = new VaultClient(vaultProperties());
+
+ Map appIdUserIdMechanisms = applicationContext.getBeansOfType(AppIdUserIdMechanism.class);
+ if(!appIdUserIdMechanisms.isEmpty()){
+ vaultClient.setAppIdUserIdMechanism(appIdUserIdMechanisms.values().iterator().next());
+ }
+
+ return vaultClient;
}
@Bean
@@ -40,7 +54,25 @@ public class VaultBootstrapConfiguration {
}
@Bean
- public VaultPropertySourceLocator vaultPropertySourceLocator() {
- return new VaultPropertySourceLocator(vaultClient(), vaultProperties());
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = "spring.cloud.vault", name = "authentication", havingValue = "APPID")
+ public AppIdUserIdMechanism appIdUserIdMechanism(VaultProperties vaultProperties) {
+
+ String userId = vaultProperties.getAppId().getUserId();
+ Assert.hasText(userId, "UserId (spring.cloud.vault.app-id.user-id) must not be empty.");
+
+ switch (userId.toUpperCase()) {
+ case VaultProperties.AppIdProperties.IP_ADDRESS:
+ return new IpAddressUserId();
+ case VaultProperties.AppIdProperties.MAC_ADDRESS:
+ return new MacAddressUserId(vaultProperties);
+ default:
+ return new StaticUserId(vaultProperties);
+ }
}
-}
\ No newline at end of file
+
+ @Bean
+ public VaultPropertySourceLocator vaultPropertySourceLocator(ApplicationContext applicationContext) {
+ return new VaultPropertySourceLocator(vaultClient(applicationContext), vaultProperties());
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/vault/VaultClient.java b/src/main/java/org/springframework/cloud/vault/VaultClient.java
index 04159330..f0047220 100644
--- a/src/main/java/org/springframework/cloud/vault/VaultClient.java
+++ b/src/main/java/org/springframework/cloud/vault/VaultClient.java
@@ -17,42 +17,54 @@
package org.springframework.cloud.vault;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
+import lombok.Value;
-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.cloud.vault.VaultProperties.AppIdProperties;
+import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
+import org.springframework.http.*;
+import org.springframework.util.Assert;
+import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
/**
+ * Vault client. This client reads data from Vault secret backends and can authenticate with
+ * Vault to obtain an access token.
+ *
* @author Spencer Gibb
+ * @author Mark Paluch
*/
@RequiredArgsConstructor
public class VaultClient {
- public static final String VAULT_TOKEN = "X-Vault-Token";
-// protected static final ParameterizedTypeReference> typeRef = new ParameterizedTypeReference>() { };
+ public static final String API_VERSION = "v1";
+ public static final String VAULT_TOKEN = "X-Vault-Token";
@Setter
private RestTemplate rest = new RestTemplate();
+ @Setter
+ private AppIdUserIdMechanism appIdUserIdMechanism;
+
private final VaultProperties properties;
- public Map read(String key) {
- String url = String.format("%s://%s:%s/v1/{backend}/{key}",
- this.properties.getScheme(), this.properties.getHost(), this.properties.getPort());
+ public Map read(String key, VaultToken vaultToken) {
- HttpHeaders headers = new HttpHeaders();
- headers.add(VAULT_TOKEN, this.properties.getToken());
+ Assert.hasText(key, "Key must not be empty!");
+ Assert.notNull(vaultToken, "VaultToken must not be null!");
+
+ String url = buildUrl();
+
+ HttpHeaders headers = createHeaders(vaultToken);
try {
- ResponseEntity response = this.rest.exchange(url, HttpMethod.GET,
- new HttpEntity<>(headers), VaultResponse.class, this.properties.getBackend(), key);
+ ResponseEntity response = this.rest.exchange(url,
+ HttpMethod.GET, new HttpEntity<>(headers), VaultResponse.class,
+ this.properties.getBackend(), key);
HttpStatus status = response.getStatusCode();
if (status == HttpStatus.OK) {
@@ -67,4 +79,79 @@ public class VaultClient {
return Collections.emptyMap();
}
-}
\ No newline at end of file
+
+ private HttpHeaders createHeaders(VaultToken vaultToken) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(VAULT_TOKEN, vaultToken.getToken());
+ return headers;
+ }
+
+ /**
+ * Creates a token using a configured authentication mechanism.
+ *
+ * @return
+ */
+ public VaultToken createToken() {
+
+ if (properties.getAuthentication() == AuthenticationMethod.APPID && appIdUserIdMechanism != null) {
+ AppIdProperties appId = properties.getAppId();
+ return createTokenUsingAppId(new AppIdTuple(properties.getApplicationName(), appIdUserIdMechanism.createUserId()), appId);
+ }
+
+ throw new UnsupportedOperationException(String.format(
+ "Cannot create a token for auth method %s", properties.getAuthentication()));
+ }
+
+ private VaultToken createTokenUsingAppId(AppIdTuple appIdTuple, AppIdProperties appId) {
+
+ String url = buildUrl();
+ Map variables = new HashMap<>();
+ variables.put("backend", "auth/" + appId.getAppIdPath());
+ variables.put("key", "login");
+
+ Map login = getAppIdLogin(appIdTuple);
+
+ try {
+ ResponseEntity response = this.rest.exchange(url,
+ HttpMethod.POST, new HttpEntity<>(login), VaultResponse.class,
+ variables);
+
+ HttpStatus status = response.getStatusCode();
+ if (!status.is2xxSuccessful()) {
+ throw new IllegalStateException("Cannot login using app-id");
+ }
+
+ VaultResponse body = response.getBody();
+ String token = (String) body.getAuth().get("client_token");
+
+ return VaultToken.of(token, body.getLeaseDuration());
+ } catch (HttpClientErrorException e) {
+
+ if (e.getStatusCode().equals(HttpStatus.BAD_REQUEST)) {
+ throw new IllegalStateException(String.format(
+ "Cannot login using app-id: %s", e.getResponseBodyAsString()));
+ }
+
+ throw e;
+ }
+ }
+
+ private Map getAppIdLogin(AppIdTuple appIdTuple) {
+
+ Map login = new HashMap<>();
+ login.put("app_id", appIdTuple.getAppId());
+ login.put("user_id", appIdTuple.getUserId());
+ return login;
+ }
+
+ private String buildUrl() {
+ return String.format("%s://%s:%s/%s/{backend}/{key}", this.properties.getScheme(),
+ this.properties.getHost(), this.properties.getPort(), API_VERSION);
+ }
+
+ @Value
+ private static class AppIdTuple{
+ private String appId;
+ private String userId;
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/vault/VaultProperties.java b/src/main/java/org/springframework/cloud/vault/VaultProperties.java
index 5c215d39..f6f081e3 100644
--- a/src/main/java/org/springframework/cloud/vault/VaultProperties.java
+++ b/src/main/java/org/springframework/cloud/vault/VaultProperties.java
@@ -24,29 +24,100 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Spencer Gibb
+ * @author Mark Paluch
*/
@ConfigurationProperties("spring.cloud.vault")
@Data
public class VaultProperties {
+
+ /**
+ * Enable Vault config server.
+ */
private boolean enabled = true;
+ /**
+ * Vault server host.
+ */
@NotEmpty
private String host = "127.0.0.1";
+ /**
+ * Vault server port.
+ */
@Range(min = 1, max = 65535)
private int port = 8200;
+ /**
+ * Protocol scheme. Can be either "http" or "https".
+ */
private String scheme = "http";
+ /**
+ * Name of the default backend.
+ */
@NotEmpty
private String backend = "secret";
+ /**
+ * Name of the default context.
+ */
@NotEmpty
private String defaultContext = "application";
+ /**
+ * Profile-separator to combine application name and profile.
+ */
@NotEmpty
private String profileSeparator = ",";
- @NotEmpty
+ /**
+ * Static vault token. Required if {@link #authentication} is {@code TOKEN}.
+ */
private String token;
-}
\ No newline at end of file
+
+ private AppIdProperties appId = new AppIdProperties();
+
+ /**
+ * Application name for AppId authentication.
+ */
+ @org.springframework.beans.factory.annotation.Value("${spring.application.name:application}")
+ private String applicationName;
+
+ private AuthenticationMethod authentication = AuthenticationMethod.TOKEN;
+
+ @Data
+ public static class AppIdProperties {
+
+ /**
+ * Property value for UserId generation using a Mac-Address.
+ * @see MacAddressUserId
+ */
+ public final static String MAC_ADDRESS = "MAC_ADDRESS";
+
+ /**
+ * Property value for UserId generation using an IP-Address.
+ * @see IpAddressUserId
+ */
+ public final static String IP_ADDRESS = "IP_ADDRESS";
+
+ /**
+ * Mount path of the AppId authentication backend.
+ */
+ private String appIdPath = "app-id";
+
+ /**
+ * Network interface hint for the "MAC_ADDRESS" UserId mechanism.
+ */
+ private String networkInterface = null;
+
+ /**
+ * UserId mechanism. Can be either "MAC_ADDRESS", "IP_ADDRESS". Any other values are passed as UserId.
+ */
+ @NotEmpty
+ private String userId = MAC_ADDRESS;
+ }
+
+ public enum AuthenticationMethod {
+ TOKEN, APPID,
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java b/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java
index a2f4e298..4777073d 100644
--- a/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java
+++ b/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java
@@ -22,35 +22,73 @@ import java.util.Set;
import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
+import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.core.env.EnumerablePropertySource;
+import org.springframework.util.Assert;
/**
* @author Spencer Gibb
+ * @author Mark Paluch
*/
@CommonsLog
public class VaultPropertySource extends EnumerablePropertySource {
+ private final VaultProperties vaultProperties;
+
private String context;
-
private Map properties = new LinkedHashMap<>();
+
+ private transient VaultState vaultState;
- public VaultPropertySource(String context, VaultClient source) {
+ public VaultPropertySource(String context, VaultClient source, VaultProperties properties, VaultState state) {
super(context, source);
this.context = context;
+ this.vaultProperties = properties;
+ this.vaultState = state;
}
public void init() {
+
try {
- Map values = this.source.read(this.context);
+ Map values = this.source.read(this.context, obtainToken());
if (values != null) {
this.properties.putAll(values);
}
} catch (Exception e) {
- log.error("Unable to read properties from vault for key "+this.context, e);
+ log.error("Unable to read properties from vault for key " + this.context, e);
}
}
+ private VaultToken obtainToken() {
+
+ if (vaultState.getToken() != null) {
+ return vaultState.getToken();
+ }
+
+ if (vaultProperties.getAuthentication() == AuthenticationMethod.TOKEN) {
+
+ Assert.hasText(vaultProperties.getToken(), "Token must not be empty");
+ vaultState.setToken(VaultToken.of(vaultProperties.getToken()));
+
+ return vaultState.getToken();
+ }
+
+ if (vaultProperties.getAuthentication() == AuthenticationMethod.APPID) {
+
+ AppIdProperties appId = vaultProperties.getAppId();
+ Assert.hasText(vaultProperties.getApplicationName(), "AppId must not be empty");
+ Assert.hasText(appId.getAppIdPath(), "AppIdPath must not be empty");
+
+ vaultState.setToken(source.createToken());
+ return vaultState.getToken();
+ }
+
+ throw new IllegalStateException(
+ String.format("Authentication method %s not supported", vaultProperties.getAuthentication()));
+ }
+
@Override
public Object getProperty(String name) {
return this.properties.get(name);
diff --git a/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java b/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java
index 3273d491..d51de11f 100644
--- a/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java
+++ b/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java
@@ -35,6 +35,7 @@ public class VaultPropertySourceLocator implements PropertySourceLocator {
private VaultClient vault;
private VaultProperties properties;
+ private transient final VaultState vaultState = new VaultState();
public VaultPropertySourceLocator(VaultClient vault, VaultProperties properties) {
this.vault = vault;
@@ -74,7 +75,7 @@ public class VaultPropertySourceLocator implements PropertySourceLocator {
}
private VaultPropertySource create(String context) {
- return new VaultPropertySource(context, this.vault);
+ return new VaultPropertySource(context, this.vault, this.properties, this.vaultState);
}
private void addProfiles(List contexts, String baseContext,
diff --git a/src/main/java/org/springframework/cloud/vault/VaultResponse.java b/src/main/java/org/springframework/cloud/vault/VaultResponse.java
index de33f617..e50b4362 100644
--- a/src/main/java/org/springframework/cloud/vault/VaultResponse.java
+++ b/src/main/java/org/springframework/cloud/vault/VaultResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2015 the original author or authors.
+ * 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.
@@ -24,11 +24,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Spencer Gibb
+ * @author Mark Paluch
*/
@Data
public class VaultResponse {
- private String auth;
+ private Map auth;
private Map data;
+ private Map metadata;
@JsonProperty("lease_duration")
private long leaseDuration;
@JsonProperty("lease_id")
diff --git a/src/main/java/org/springframework/cloud/vault/VaultState.java b/src/main/java/org/springframework/cloud/vault/VaultState.java
new file mode 100644
index 00000000..96207534
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/vault/VaultState.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 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.vault;
+
+import lombok.Data;
+
+/**
+ * State of the Vault client.
+ *
+ * @author Mark Paluch
+ */
+@Data
+class VaultState {
+ private VaultToken token;
+}
diff --git a/src/main/java/org/springframework/cloud/vault/VaultToken.java b/src/main/java/org/springframework/cloud/vault/VaultToken.java
new file mode 100644
index 00000000..292048e9
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/vault/VaultToken.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 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.vault;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Value;
+
+import org.springframework.util.Assert;
+
+/**
+ * @author Mark Paluch
+ */
+@Value
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class VaultToken {
+ private String token;
+ private long leaseDuration;
+
+ public static VaultToken of(String token) {
+ return of(token, 0);
+ }
+
+ public static VaultToken of(String token, long leaseDuration) {
+ Assert.hasText(token, "Token must not be empty");
+ return new VaultToken(token, leaseDuration);
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/VaultAppIdCustomMechanismTests.java b/src/test/java/org/springframework/cloud/vault/VaultAppIdCustomMechanismTests.java
new file mode 100644
index 00000000..9eefeb3c
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/VaultAppIdCustomMechanismTests.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 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.vault;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Collections;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.cloud.vault.VaultAppIdCustomMechanismTests.BootstrapConfiguration;
+import org.springframework.cloud.vault.util.Settings;
+import org.springframework.cloud.vault.util.VaultRule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+/**
+ * @author Mark Paluch
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = {BootstrapConfiguration.class, VaultAppIdCustomMechanismTests.TestApplication.class})
+@IntegrationTest({ "spring.cloud.vault.authentication=appid", "use.custom.config=true", "spring.application.name=VaultAppIdCustomMechanismTests"})
+public class VaultAppIdCustomMechanismTests {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+
+ VaultRule vaultRule = new VaultRule();
+ vaultRule.before();
+
+ vaultRule.prepare().writeSecret(VaultAppIdCustomMechanismTests.class.getSimpleName(), Collections.singletonMap("vault.value", "foo"));
+
+ VaultProperties vaultProperties = Settings.createVaultProperties();
+ vaultProperties.setAuthentication(VaultProperties.AuthenticationMethod.APPID);
+
+ if (!vaultRule.prepare().hasAuth(vaultProperties.getAppId().getAppIdPath())) {
+ vaultRule.prepare().mountAuth(vaultProperties.getAppId().getAppIdPath());
+ }
+
+ vaultRule.prepare().mapAppId(VaultAppIdCustomMechanismTests.class.getSimpleName());
+ vaultRule.prepare().mapUserId(VaultAppIdCustomMechanismTests.class.getSimpleName(), new StaticUserIdMechanism().createUserId());
+
+ }
+
+ @Value("${vault.value}") String configValue;
+
+ @Test
+ public void contextLoads() {
+
+ assertThat(configValue).isEqualTo("foo");
+ }
+
+ @SpringBootApplication
+ public static class TestApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TestApplication.class, args);
+ }
+ }
+
+ @Configuration
+ public static class BootstrapConfiguration {
+
+ @Bean
+ @ConditionalOnProperty("use.custom.config")
+ AppIdUserIdMechanism appIdUserIdMechanism() {
+ return new StaticUserIdMechanism();
+ }
+ }
+
+ public static class StaticUserIdMechanism implements AppIdUserIdMechanism {
+
+ @Override
+ public String createUserId() {
+ return "static-string";
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/VaultAppIdTests.java b/src/test/java/org/springframework/cloud/vault/VaultAppIdTests.java
new file mode 100644
index 00000000..af052a27
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/VaultAppIdTests.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 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.vault;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Collections;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.cloud.vault.util.Settings;
+import org.springframework.cloud.vault.util.VaultRule;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+/**
+ * @author Mark Paluch
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = VaultAppIdTests.TestApplication.class)
+@IntegrationTest({"spring.cloud.vault.authentication=appid", "spring.cloud.vault.app-id.user-id=IP_ADDRESS", "spring.application.name=VaultAppIdTests"})
+public class VaultAppIdTests {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+
+ VaultRule vaultRule = new VaultRule();
+ vaultRule.before();
+
+ vaultRule.prepare().writeSecret(VaultAppIdTests.class.getSimpleName(), Collections.singletonMap("vault.value", "foo"));
+
+ VaultProperties vaultProperties = Settings.createVaultProperties();
+ vaultProperties.setAuthentication(VaultProperties.AuthenticationMethod.APPID);
+ vaultProperties.getAppId().setUserId(VaultProperties.AppIdProperties.IP_ADDRESS);
+
+ if (!vaultRule.prepare().hasAuth(vaultProperties.getAppId().getAppIdPath())) {
+ vaultRule.prepare().mountAuth(vaultProperties.getAppId().getAppIdPath());
+ }
+
+ vaultRule.prepare().mapAppId(VaultAppIdTests.class.getSimpleName());
+ vaultRule.prepare().mapUserId(VaultAppIdTests.class.getSimpleName(), new IpAddressUserId().createUserId());
+ }
+
+ @Value("${vault.value}")
+ String configValue;
+
+ @Test
+ public void contextLoads() {
+
+ assertThat(configValue).isEqualTo("foo");
+ }
+
+ @SpringBootApplication
+ public static class TestApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TestApplication.class, args);
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/VaultTests.java b/src/test/java/org/springframework/cloud/vault/VaultTests.java
index 58a1513e..bd67192c 100644
--- a/src/test/java/org/springframework/cloud/vault/VaultTests.java
+++ b/src/test/java/org/springframework/cloud/vault/VaultTests.java
@@ -1,20 +1,44 @@
package org.springframework.cloud.vault;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
+import org.springframework.cloud.vault.util.PrepareVault;
+import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = VaultTests.TestApplication.class)
-@WebIntegrationTest(randomPort = true)
public class VaultTests {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+
+ VaultRule vaultRule = new VaultRule();
+ vaultRule.before();
+
+ vaultRule.prepare().writeSecret("testVaultApp", Collections.singletonMap("vault.value", "foo"));
+ }
+
+ @Value("${vault.value}")
+ String configValue;
+
@Test
public void contextLoads() {
+
+ assertThat(configValue).isEqualTo("foo");
}
@SpringBootApplication
diff --git a/src/test/java/org/springframework/cloud/vault/integration/AbstractIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/AbstractIntegrationTests.java
new file mode 100644
index 00000000..207d95d7
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/integration/AbstractIntegrationTests.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 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.vault.integration;
+
+import org.junit.Rule;
+import org.springframework.cloud.vault.util.PrepareVault;
+import org.springframework.cloud.vault.util.VaultRule;
+
+/**
+ * Base class for integration tests using Vault.
+ *
+ * @author Mark Paluch
+ */
+public abstract class AbstractIntegrationTests {
+
+ @Rule
+ public final VaultRule vaultRule = new VaultRule();
+
+ public final PrepareVault prepare() {
+ return vaultRule.prepare();
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationIntegrationTests.java
new file mode 100644
index 00000000..78c2ed20
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationIntegrationTests.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 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.vault.integration;
+
+import org.junit.Before;
+import org.springframework.cloud.vault.IpAddressUserId;
+import org.springframework.cloud.vault.VaultClient;
+import org.springframework.cloud.vault.VaultProperties;
+import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
+import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
+import org.springframework.cloud.vault.VaultToken;
+import org.springframework.cloud.vault.util.Settings;
+
+/**
+ * Integration tests for {@link VaultClient} using {@link AuthenticationMethod#APPID}.
+ *
+ * @author Mark Paluch
+ */
+public class AppIdAuthenticationIntegrationTests extends GenericSecretIntegrationTests {
+
+ private VaultClient vaultClient;
+
+ @Before
+ public void setUp() throws Exception {
+
+ super.setUp();
+
+ VaultProperties vaultProperties = Settings.createVaultProperties();
+
+ AppIdProperties appId = configureAppIdProperties();
+ vaultProperties.setApplicationName("myapp");
+ vaultProperties.setAuthentication(AuthenticationMethod.APPID);
+ vaultProperties.setAppId(appId);
+
+ if (!prepare().hasAuth(appId.getAppIdPath())) {
+ prepare().mountAuth(appId.getAppIdPath());
+ }
+
+ IpAddressUserId userIdMechanism = new IpAddressUserId();
+ String userId = userIdMechanism.createUserId();
+ prepare().mapAppId(vaultProperties.getApplicationName());
+ prepare().mapUserId(vaultProperties.getApplicationName(), userId);
+
+ vaultClient = new VaultClient(vaultProperties);
+ vaultClient.setAppIdUserIdMechanism(userIdMechanism);
+
+ }
+
+ @Override
+ protected VaultToken createToken() {
+ return vaultClient.createToken();
+ }
+
+ private AppIdProperties configureAppIdProperties() {
+
+ AppIdProperties appId = new AppIdProperties();
+ appId.setUserId(AppIdProperties.IP_ADDRESS);
+ return appId;
+ }
+
+}
diff --git a/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationMethodsIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationMethodsIntegrationTests.java
new file mode 100644
index 00000000..3cc0b5bb
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationMethodsIntegrationTests.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 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.vault.integration;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.cloud.vault.*;
+import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
+import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
+import org.springframework.cloud.vault.util.Settings;
+
+/**
+ * Integration tests for {@link VaultClient} using various UserIds.
+ *
+ * @author Mark Paluch
+ */
+public class AppIdAuthenticationMethodsIntegrationTests extends AbstractIntegrationTests {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Before
+ public void setUp() throws Exception {
+ prepare().mapAppId("myapp");
+ }
+
+ @Test
+ public void loginUsingIpAddressShouldCreateAToken() throws Exception {
+
+ VaultClient vaultClient = new VaultClient(
+ prepareAppIdAuthenticationMethod(AppIdProperties.IP_ADDRESS, "myapp"));
+ vaultClient.setAppIdUserIdMechanism(new IpAddressUserId());
+ assertThat(vaultClient.createToken()).isNotNull();
+ }
+
+ @Test
+ public void loginUsingStaticUserIdShouldCreateAToken() throws Exception {
+
+ VaultProperties vaultProperties = prepareAppIdAuthenticationMethod("my-user-id", "myapp");
+ VaultClient vaultClient = new VaultClient(vaultProperties);
+ vaultClient.setAppIdUserIdMechanism(new StaticUserId(vaultProperties));
+ assertThat(vaultClient.createToken()).isNotNull();
+ }
+
+ @Test
+ public void loginUsingMacAddressShouldCreateAToken() throws Exception {
+
+ VaultProperties vaultProperties = prepareAppIdAuthenticationMethod(AppIdProperties.MAC_ADDRESS, "myapp");
+ VaultClient vaultClient = new VaultClient(vaultProperties);
+
+ vaultClient.setAppIdUserIdMechanism(
+ new MacAddressUserId(vaultProperties));
+ assertThat(vaultClient.createToken()).isNotNull();
+ }
+
+ @Test
+ public void invalidLogin() throws Exception {
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cannot login using app-id");
+
+ VaultProperties vaultProperties = prepareAppIdAuthenticationMethod(
+ AppIdProperties.IP_ADDRESS, "myapp");
+ vaultProperties.setApplicationName("foobar");
+
+ VaultClient vaultClient = new VaultClient(vaultProperties);
+ vaultClient.setAppIdUserIdMechanism(new MacAddressUserId(vaultProperties));
+
+ vaultClient.createToken();
+
+ fail("Missing IllegalStateException");
+
+ }
+
+ private VaultProperties prepareAppIdAuthenticationMethod(String userId, String appId)
+ throws SocketException {
+
+ VaultProperties vaultProperties = Settings.createVaultProperties();
+
+ AppIdProperties appIdProperties = new AppIdProperties();
+ vaultProperties.setApplicationName(appId);
+ appIdProperties.setUserId(userId);
+
+ Enumeration networkInterfaces = NetworkInterface
+ .getNetworkInterfaces();
+ NetworkInterface networkInterface = null;
+ while (networkInterfaces.hasMoreElements()) {
+ networkInterface = networkInterfaces.nextElement();
+ if (networkInterface.getHardwareAddress() != null) {
+ break;
+ }
+ }
+
+ // make sure we have always a network interface even if the localhost reverse
+ // lookup maps to an IP address that is not handled by this host.
+ appIdProperties.setNetworkInterface(networkInterface.getName());
+
+ vaultProperties.setAuthentication(AuthenticationMethod.APPID);
+ vaultProperties.setAppId(appIdProperties);
+
+ String userIdValue;
+ if (userId.equals(AppIdProperties.IP_ADDRESS)) {
+ userIdValue = new IpAddressUserId().createUserId();
+ }
+ else if (userId.equals(AppIdProperties.MAC_ADDRESS)) {
+ userIdValue = new MacAddressUserId(vaultProperties).createUserId();
+ }
+ else {
+ userIdValue = userId;
+ }
+
+ prepare().mapUserId(vaultProperties.getApplicationName(), userIdValue);
+
+ return vaultProperties;
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/integration/GenericSecretIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/GenericSecretIntegrationTests.java
new file mode 100644
index 00000000..373f8dc9
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/integration/GenericSecretIntegrationTests.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 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.vault.integration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.cloud.vault.VaultClient;
+import org.springframework.cloud.vault.VaultProperties;
+import org.springframework.cloud.vault.VaultToken;
+import org.springframework.cloud.vault.util.Settings;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Integration tests for {@link VaultClient} using the generic secret backend.
+ * @author Mark Paluch
+ */
+public class GenericSecretIntegrationTests extends AbstractIntegrationTests {
+
+ private VaultProperties vaultProperties = Settings.createVaultProperties();
+ private VaultClient vaultClient = new VaultClient(vaultProperties);
+
+ @Before
+ public void setUp() throws Exception {
+
+ prepare().writeSecret("app-name", (Map) createData());
+ vaultClient.setRest(new RestTemplate());
+ }
+
+ @Test
+ public void shouldReturnSecretsCorrectly() throws Exception {
+
+ Map secretProperties = vaultClient.read("app-name",
+ createToken());
+
+ assertThat(secretProperties).containsAllEntriesOf(createExpectedMap());
+ }
+
+ @Test
+ public void shouldReturnNullIfNotFound() throws Exception {
+
+ Map secretProperties = vaultClient.read("missing", createToken());
+
+ assertThat(secretProperties).isNull();
+ }
+
+ /**
+ * Can be overridden by subclasses.
+ *
+ * @return
+ */
+ protected VaultToken createToken() {
+ return Settings.token();
+ }
+
+ private Map createData() {
+ Map data = new HashMap<>();
+ data.put("string", "value");
+ data.put("number", "1234");
+ data.put("boolean", true);
+ return data;
+ }
+
+ private Map createExpectedMap() {
+ Map data = new HashMap<>();
+ data.put("string", "value");
+ data.put("number", "1234");
+ data.put("boolean", "true");
+ return data;
+ }
+
+}
diff --git a/src/test/java/org/springframework/cloud/vault/integration/PrepareVaultTests.java b/src/test/java/org/springframework/cloud/vault/integration/PrepareVaultTests.java
new file mode 100644
index 00000000..01f96ce6
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/integration/PrepareVaultTests.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 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.vault.integration;
+
+import org.junit.Test;
+import org.springframework.boot.test.TestRestTemplate;
+import org.springframework.cloud.vault.VaultProperties;
+import org.springframework.cloud.vault.VaultToken;
+import org.springframework.cloud.vault.util.PrepareVault;
+import org.springframework.cloud.vault.util.Settings;
+
+/**
+ * Integration tests for {@link PrepareVault}.
+ *
+ * @author Mark Paluch
+ */
+public class PrepareVaultTests {
+
+ private VaultProperties vaultProperties = Settings.createVaultProperties();
+ private PrepareVault prepareVault = new PrepareVault(new TestRestTemplate());
+
+ @Test
+ public void initializeShouldCreateANewVault() throws Exception {
+
+ prepareVault.setRootToken(Settings.token());
+ prepareVault.setVaultProperties(vaultProperties);
+
+ if (!prepareVault.isAvailable()) {
+ VaultToken rootToken = prepareVault.initializeVault();
+ prepareVault.setRootToken(rootToken);
+ prepareVault.createToken(vaultProperties.getToken(), "root");
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/util/PrepareVault.java b/src/test/java/org/springframework/cloud/vault/util/PrepareVault.java
new file mode 100644
index 00000000..b52a622b
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/util/PrepareVault.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright 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.vault.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.springframework.cloud.vault.VaultClient;
+import org.springframework.cloud.vault.VaultProperties;
+import org.springframework.cloud.vault.VaultToken;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestTemplate;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Data;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.Value;
+
+/**
+ * Test helper to prepare various settings within Vault.
+ *
+ * @author Mark Paluch
+ */
+public class PrepareVault {
+
+ public final static String INITIALIZE_URL_TEMPLATE = "{baseuri}/sys/init";
+ public final static String MOUNT_AUTH_URL_TEMPLATE = "{baseuri}/sys/auth/{authBackend}";
+ public final static String SYS_AUTH_URL_TEMPLATE = "{baseuri}/sys/auth";
+ public final static String SEAL_STATUS_URL_TEMPLATE = "{baseuri}/sys/seal-status";
+ public final static String UNSEAL_URL_TEMPLATE = "{baseuri}/sys/unseal";
+ public final static String CREATE_TOKEN_URL_TEMPLATE = "{baseuri}/auth/token/create-orphan";
+ public final static String WRITE_URL_TEMPLATE = "{baseuri}/{path}";
+ public static final ParameterizedTypeReference>> MAP_OF_MAPS_TYPE = new ParameterizedTypeReference>>() {
+
+ };
+
+ private final RestTemplate restTemplate;
+
+ @Setter
+ @NonNull
+ private VaultProperties vaultProperties;
+
+ @Setter
+ @NonNull
+ private VaultToken rootToken;
+
+ public PrepareVault(RestTemplate restTemplate) {
+
+ Assert.notNull(restTemplate, "RestTemplate must not be null");
+ this.restTemplate = restTemplate;
+ }
+
+ /**
+ * Initialize Vault and unseal the vault.
+ *
+ * @return the root token.
+ */
+ public VaultToken initializeVault() {
+
+ Assert.notNull(vaultProperties, "VaultProperties must not be null");
+
+ Map parameters = parameters(vaultProperties);
+
+ int createKeys = 2;
+ int requiredKeys = 2;
+
+ InitializeVault initializeVault = InitializeVault.of(createKeys, requiredKeys);
+
+ ResponseEntity initResponse = restTemplate.exchange(
+ INITIALIZE_URL_TEMPLATE, HttpMethod.PUT,
+ new HttpEntity<>(initializeVault), VaultInitialized.class, parameters);
+
+ if (!initResponse.getStatusCode().is2xxSuccessful()) {
+ throw new IllegalStateException(
+ "Cannot initialize vault: " + initResponse.toString());
+ }
+ VaultInitialized initialized = initResponse.getBody();
+
+ for (int i = 0; i < requiredKeys; i++) {
+
+ UnsealKey unsealKey = UnsealKey.of(initialized.getKeys().get(i));
+ ResponseEntity unsealResponse = restTemplate.exchange(
+ UNSEAL_URL_TEMPLATE, HttpMethod.PUT, new HttpEntity<>(unsealKey),
+ UnsealProgress.class, parameters);
+
+ UnsealProgress unsealProgress = unsealResponse.getBody();
+ if (!unsealProgress.isSealed()) {
+ break;
+ }
+ }
+
+ return VaultToken.of(initialized.getRootToken());
+ }
+
+ /**
+ * Create a token for the given {@code tokenId} and {@code policy}.
+ *
+ * @param tokenId
+ * @param policy
+ * @return
+ */
+ public VaultToken createToken(String tokenId, String policy) {
+
+ Map parameters = parameters(vaultProperties);
+
+ CreateToken createToken = new CreateToken();
+ createToken.setId(tokenId);
+ if (policy != null) {
+ createToken.setPolicies(Collections.singletonList(policy));
+ }
+
+ HttpHeaders headers = authenticatedHeaders();
+
+ HttpEntity entity = new HttpEntity<>(createToken, headers);
+
+ ResponseEntity createTokenResponse = restTemplate.exchange(
+ CREATE_TOKEN_URL_TEMPLATE, HttpMethod.POST, entity, TokenCreated.class,
+ parameters);
+
+ if (!createTokenResponse.getStatusCode().is2xxSuccessful()) {
+ throw new IllegalStateException(
+ "Cannot create token: " + createTokenResponse.toString());
+ }
+
+ AuthToken authToken = createTokenResponse.getBody().getAuth();
+
+ return VaultToken.of(authToken.getClientToken());
+ }
+
+ /**
+ * Check whether Vault is available (vault created and unsealed).
+ *
+ * @return
+ */
+ public boolean isAvailable() {
+
+ Map parameters = parameters(vaultProperties);
+
+ ResponseEntity exchange = restTemplate
+ .getForEntity(SEAL_STATUS_URL_TEMPLATE, String.class, parameters);
+
+ if (exchange.getStatusCode().is2xxSuccessful()) {
+ return true;
+ }
+
+ if (exchange.getStatusCode().is4xxClientError()) {
+ return false;
+ }
+ throw new IllegalStateException("Vault error: " + exchange.toString());
+ }
+
+ /**
+ * Mount an auth backend.
+ *
+ * @param authBackend
+ */
+ public void mountAuth(String authBackend) {
+
+ Assert.hasText(authBackend, "AuthBackend must not be empty");
+
+ Map parameters = parameters(vaultProperties);
+ parameters.put("authBackend", authBackend);
+
+ Map requestEntity = Collections.singletonMap("type", authBackend);
+
+ HttpEntity> entity = new HttpEntity<>(requestEntity,
+ authenticatedHeaders());
+
+ ResponseEntity responseEntity = restTemplate.exchange(
+ MOUNT_AUTH_URL_TEMPLATE, HttpMethod.POST, entity, String.class,
+ parameters);
+
+ if (!responseEntity.getStatusCode().is2xxSuccessful()) {
+ throw new IllegalStateException(
+ "Cannot create mount auth backend: " + responseEntity.toString());
+ }
+
+ responseEntity.getBody();
+ }
+
+ /**
+ * Check whether a auth-backend is enabled.
+ *
+ * @param authBackend
+ * @return
+ */
+ public boolean hasAuth(String authBackend) {
+
+ Assert.hasText(authBackend, "AuthBackend must not be empty");
+
+ Map parameters = parameters(vaultProperties);
+ parameters.put("authBackend", authBackend);
+
+ HttpEntity> entity = new HttpEntity<>(authenticatedHeaders());
+
+ ResponseEntity>> responseEntity = restTemplate
+ .exchange(SYS_AUTH_URL_TEMPLATE, HttpMethod.GET, entity, MAP_OF_MAPS_TYPE,
+ parameters);
+
+ if (!responseEntity.getStatusCode().is2xxSuccessful()) {
+ throw new IllegalStateException(
+ "Cannot create mount auth backend: " + responseEntity.toString());
+ }
+
+ Map> body = responseEntity.getBody();
+ for (Entry> entry : body.entrySet()) {
+ if (entry.getKey().contains(authBackend)
+ && authBackend.equals(entry.getValue().get("type"))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Write key-value data to the Vault secret backend.
+ *
+ * @param path
+ * @param data
+ */
+ public void writeSecret(String path, Map data) {
+
+ Assert.hasText(path, "Path must not be empty");
+ write("secret/" + path, data);
+ }
+
+ /**
+ * Write key-value data to a path in Vault.
+ *
+ * @param path
+ * @param data
+ */
+ public void write(String path, Map data) {
+
+ Assert.hasText(path, "Path must not be empty");
+ Assert.notNull(data, "Data must not be null");
+
+ HttpHeaders headers = authenticatedHeaders();
+
+ Map parameters = parameters(vaultProperties);
+ parameters.put("path", path);
+
+ ResponseEntity exchange = restTemplate.exchange(WRITE_URL_TEMPLATE,
+ HttpMethod.PUT, new HttpEntity(data, headers), String.class,
+ parameters);
+
+ if (!exchange.getStatusCode().is2xxSuccessful()) {
+ throw new IllegalStateException(
+ String.format("Cannot write to %s: %s", path, exchange.getBody()));
+ }
+ }
+
+ /**
+ * Create an userId to appId mapping.
+ *
+ * @param appId
+ * @param userId
+ */
+ public void mapUserId(String appId, String userId) {
+
+ Map userIdData = new HashMap<>();
+ userIdData.put("value", appId); // name of the app-id
+ userIdData.put("cidr_block", "0.0.0.0/0");
+
+ String appIdPath = vaultProperties.getAppId().getAppIdPath();
+ if (!hasAuth(appIdPath)) {
+ mountAuth(appIdPath);
+ }
+
+ write(String.format("auth/%s/map/user-id/%s", appIdPath, userId), userIdData);
+ }
+
+ /**
+ * Create an appId mapping.
+ *
+ * @param appId
+ */
+ public void mapAppId(String appId) {
+
+ Map appIdData = new HashMap<>();
+ appIdData.put("value", "root"); // policy
+ appIdData.put("display_name", "this is my test application");
+
+ write(String.format("auth/%s/map/app-id/%s",
+ vaultProperties.getAppId().getAppIdPath(), appId), appIdData);
+ }
+
+ private HttpHeaders authenticatedHeaders() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(VaultClient.VAULT_TOKEN, rootToken.getToken());
+ return headers;
+ }
+
+ private Map parameters(VaultProperties vaultProperties) {
+
+ Map parameters = new HashMap<>();
+
+ String baseUri = String.format("%s://%s:%s/%s", vaultProperties.getScheme(),
+ vaultProperties.getHost(), vaultProperties.getPort(),
+ VaultClient.API_VERSION);
+ parameters.put("baseuri", baseUri);
+
+ return parameters;
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ @Data
+ static class TokenCreated {
+
+ @JsonProperty("lease_duration")
+ private long leaseDuration;
+ @JsonProperty("renewable")
+ private boolean renewable;
+ @JsonProperty("auth")
+ private AuthToken auth;
+
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ @Value(staticConstructor = "of")
+ static class InitializeVault {
+
+ @JsonProperty("secret_shares")
+ private int secretShares;
+
+ @JsonProperty("secret_threshold")
+ private int secretThreshold;
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ @Data
+ static class CreateToken {
+
+ @JsonProperty("id")
+ private String id;
+
+ @JsonProperty("policies")
+ private List policies;
+
+ @JsonProperty("ttl")
+ private String ttl;
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ @Value(staticConstructor = "of")
+ static class UnsealKey {
+ @JsonProperty
+ @NonNull
+ private String key;
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ @Data
+ static class UnsealProgress {
+
+ @JsonProperty("sealed")
+ private boolean sealed;
+ @JsonProperty("t")
+ private int t;
+ @JsonProperty("n")
+ private int n;
+ @JsonProperty("progress")
+ private int progress;
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ @Data
+ static class VaultInitialized {
+
+ @JsonProperty("keys")
+ private List keys;
+ @JsonProperty("root_token")
+ private String rootToken;
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ @Data
+ public static class AuthToken {
+
+ @JsonProperty("client_token")
+ private String clientToken;
+
+ @JsonProperty("policies")
+ private List policies;
+
+ @JsonProperty("metadata")
+ private Map metadata;
+
+ @JsonProperty("lease_duration")
+ private long leaseDuration;
+
+ @JsonProperty("renewable")
+ private boolean renewable;
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/util/Settings.java b/src/test/java/org/springframework/cloud/vault/util/Settings.java
new file mode 100644
index 00000000..f62db9e9
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/util/Settings.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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.vault.util;
+
+import org.springframework.cloud.vault.VaultProperties;
+import org.springframework.cloud.vault.VaultToken;
+
+/**
+ * Utility to retrieve settings during test.
+ *
+ * @author Mark Paluch
+ */
+public class Settings {
+
+ /**
+ *
+ * @return the vault properties.
+ */
+ public static VaultProperties createVaultProperties() {
+
+ VaultProperties vaultProperties = new VaultProperties();
+ vaultProperties.setToken(token().getToken());
+ vaultProperties.setHost(System.getProperty("vault.host", "localhost"));
+
+ return vaultProperties;
+ }
+
+ /**
+ * @return the token to use during tests.
+ */
+ public static VaultToken token() {
+ return VaultToken.of(System.getProperty("vault.token",
+ "00000000-0000-0000-0000-000000000000"));
+ }
+}
diff --git a/src/test/java/org/springframework/cloud/vault/util/VaultRule.java b/src/test/java/org/springframework/cloud/vault/util/VaultRule.java
new file mode 100644
index 00000000..3c66a92f
--- /dev/null
+++ b/src/test/java/org/springframework/cloud/vault/util/VaultRule.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 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.vault.util;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+import org.junit.rules.ExternalResource;
+import org.springframework.boot.test.TestRestTemplate;
+import org.springframework.cloud.vault.VaultProperties;
+import org.springframework.cloud.vault.VaultToken;
+
+/**
+ * Vault rule to ensure a running and prepared Vault.
+ *
+ * @author Mark Paluch
+ */
+public class VaultRule extends ExternalResource {
+
+ private final VaultProperties vaultProperties;
+ private final PrepareVault prepareVault = new PrepareVault(new TestRestTemplate());
+
+ public VaultRule() {
+ this(Settings.createVaultProperties());
+ }
+
+ public VaultRule(VaultProperties vaultProperties) {
+ this.vaultProperties = vaultProperties;
+ }
+
+ @Override
+ public void before() {
+
+ try (Socket socket = new Socket()) {
+
+ socket.connect(new InetSocketAddress(InetAddress.getByName("localhost"),
+ vaultProperties.getPort()));
+ socket.close();
+
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(String.format(
+ "Vault is not running on localhost:%d which is required to run a test using @Rule %s",
+ vaultProperties.getPort(), getClass().getSimpleName()));
+ }
+
+ prepareVault.setVaultProperties(vaultProperties);
+
+ if (!prepareVault.isAvailable()) {
+ VaultToken rootToken = prepareVault.initializeVault();
+ prepareVault.setRootToken(rootToken);
+ prepareVault.createToken(vaultProperties.getToken(), "root");
+ }
+ else {
+ prepareVault.setRootToken(Settings.token());
+ }
+ }
+
+ public PrepareVault prepare() {
+ return prepareVault;
+ }
+
+}
diff --git a/src/test/resources/META-INF/spring.factories b/src/test/resources/META-INF/spring.factories
new file mode 100644
index 00000000..852326b6
--- /dev/null
+++ b/src/test/resources/META-INF/spring.factories
@@ -0,0 +1,3 @@
+# Bootstrap Configuration
+org.springframework.cloud.bootstrap.BootstrapConfiguration=\
+org.springframework.cloud.vault.VaultAppIdCustomMechanismTests.BootstrapConfiguration
diff --git a/src/test/resources/vault.conf b/src/test/resources/vault.conf
new file mode 100644
index 00000000..33be49bc
--- /dev/null
+++ b/src/test/resources/vault.conf
@@ -0,0 +1,7 @@
+backend "inmem" {
+}
+
+listener "tcp" {
+ address = "127.0.0.1:8200"
+ tls_disable = 1
+}
\ No newline at end of file