Skip to content

Commit cda433f

Browse files
Merge pull request #151 from jarrodcodes/master
Vault 1.0+ compatibility
2 parents 70d36df + 57c478f commit cda433f

69 files changed

Lines changed: 5024 additions & 3422 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ version '3.1.0'
88
ext.isReleaseVersion = !version.endsWith('SNAPSHOT')
99

1010
compileJava {
11-
sourceCompatibility = 1.7
12-
targetCompatibility = 1.7
11+
sourceCompatibility = 1.8
12+
targetCompatibility = 1.8
1313
}
1414

1515
compileTestJava {
@@ -22,21 +22,21 @@ repositories {
2222
}
2323

2424
dependencies {
25-
compileOnly('org.projectlombok:lombok:1.16.20')
25+
compileOnly('org.projectlombok:lombok:1.18.4')
2626

2727
testCompile('junit:junit:4.12')
28-
testCompile('org.mockito:mockito-core:2.13.0')
29-
testCompile('org.testcontainers:testcontainers:1.5.1')
30-
testCompile('org.eclipse.jetty:jetty-server:9.4.8.v20171121')
28+
testCompile('org.mockito:mockito-core:2.23.4')
29+
testCompile('org.testcontainers:testcontainers:1.6.0')
30+
testCompile('org.eclipse.jetty:jetty-server:9.4.14.v20181114')
3131
testCompile('org.slf4j:slf4j-api:1.7.25')
32-
testCompile('org.bouncycastle:bcprov-jdk15on:1.59')
33-
testCompile('org.bouncycastle:bcpkix-jdk15on:1.59')
32+
testCompile('org.bouncycastle:bcprov-jdk15on:1.60')
33+
testCompile('org.bouncycastle:bcpkix-jdk15on:1.60')
3434

3535
testRuntime('org.slf4j:slf4j-simple:1.7.25')
3636
}
3737

3838
task wrapper(type: Wrapper) {
39-
gradleVersion = '4.2.1'
39+
gradleVersion = '4.10.2'
4040
}
4141

4242
task javadocJar(type: Jar, dependsOn: javadoc) {

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists
6-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-all.zip
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip

src/main/java/com/bettercloud/vault/EnvironmentLoader.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.io.IOException;
44
import java.io.Serializable;
5+
import java.nio.charset.StandardCharsets;
56
import java.nio.file.Files;
67
import java.nio.file.Paths;
78

@@ -25,7 +26,7 @@ public String loadVariable(final String name) {
2526
try {
2627
final byte[] bytes = Files.readAllBytes(
2728
Paths.get(System.getProperty("user.home")).resolve(".vault-token"));
28-
value = new String(bytes, "UTF-8").trim();
29+
value = new String(bytes, StandardCharsets.UTF_8).trim();
2930
} catch (IOException e) {
3031
// No-op... there simply isn't a token value available
3132
}

src/main/java/com/bettercloud/vault/SslConfig.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.InputStreamReader;
2020
import java.io.ObjectInputStream;
2121
import java.io.Serializable;
22+
import java.nio.charset.StandardCharsets;
2223
import java.security.KeyFactory;
2324
import java.security.KeyManagementException;
2425
import java.security.KeyStore;
@@ -538,7 +539,7 @@ private SSLContext buildSslContextFromPem() throws VaultException {
538539
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
539540
// Convert the trusted servers PEM data into an X509Certificate
540541
X509Certificate certificate;
541-
try (final ByteArrayInputStream pem = new ByteArrayInputStream(pemUTF8.getBytes("UTF-8"))) {
542+
try (final ByteArrayInputStream pem = new ByteArrayInputStream(pemUTF8.getBytes(StandardCharsets.UTF_8))) {
542543
certificate = (X509Certificate) certificateFactory.generateCertificate(pem);
543544
}
544545
// Build a truststore
@@ -553,7 +554,7 @@ private SSLContext buildSslContextFromPem() throws VaultException {
553554
final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
554555
// Convert the client certificate PEM data into an X509Certificate
555556
X509Certificate clientCertificate;
556-
try (final ByteArrayInputStream pem = new ByteArrayInputStream(clientPemUTF8.getBytes("UTF-8"))) {
557+
try (final ByteArrayInputStream pem = new ByteArrayInputStream(clientPemUTF8.getBytes(StandardCharsets.UTF_8))) {
557558
clientCertificate = (X509Certificate) certificateFactory.generateCertificate(pem);
558559
}
559560

@@ -613,7 +614,7 @@ private KeyStore inputStreamToKeyStore(final InputStream inputStream, final Stri
613614
* @throws IOException
614615
*/
615616
private static String inputStreamToUTF8(final InputStream input) throws IOException {
616-
final BufferedReader in = new BufferedReader(new InputStreamReader(input, "UTF-8"));
617+
final BufferedReader in = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
617618
final StringBuilder utf8 = new StringBuilder("");
618619
String str;
619620
while ((str = in.readLine()) != null) {

src/main/java/com/bettercloud/vault/Vault.java

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66
import com.bettercloud.vault.api.Logical;
77
import com.bettercloud.vault.api.Seal;
88
import com.bettercloud.vault.api.pki.Pki;
9+
import com.bettercloud.vault.json.Json;
10+
import com.bettercloud.vault.json.JsonObject;
11+
import com.bettercloud.vault.json.JsonValue;
12+
import com.bettercloud.vault.rest.Rest;
13+
import com.bettercloud.vault.rest.RestException;
14+
import com.bettercloud.vault.rest.RestResponse;
15+
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.HashMap;
18+
import java.util.Map;
919

1020
/**
1121
* <p>The Vault driver class, the primary interface through which dependent applications will access Vault.</p>
@@ -50,16 +60,72 @@ public class Vault {
5060
* Construct a Vault driver instance with the provided config settings.
5161
*
5262
* @param vaultConfig Configuration settings for Vault interaction (e.g. server address, token, etc)
63+
* If the VaultConfig Engine version path map is not supplied in the config, default to global KV
64+
* engine version 2.
5365
*/
5466
public Vault(final VaultConfig vaultConfig) {
5567
this.vaultConfig = vaultConfig;
68+
if (this.vaultConfig.getNameSpace() != null && !this.vaultConfig.getNameSpace().isEmpty()) {
69+
System.out.println(String.format("The NameSpace %s has been bound to this Vault instance. Please keep this in mind when running operations.", this.vaultConfig.getNameSpace()));
70+
}
71+
if (this.vaultConfig.getSecretsEnginePathMap().isEmpty() && this.vaultConfig.getGlobalEngineVersion() == null) {
72+
System.out.println("Constructing a Vault instance with no provided Engine version, defaulting to version 2.");
73+
this.vaultConfig.setEngineVersion(2);
74+
}
75+
}
76+
77+
/**
78+
* Construct a Vault driver instance with the provided config settings, and use the provided global KV Engine version for all secrets.
79+
*/
80+
public Vault(final VaultConfig vaultConfig, final Integer engineVersion) {
81+
if (engineVersion < 1 || engineVersion > 2) {
82+
throw new IllegalArgumentException("The Engine version must be '1' or '2', the version supplied was: '"
83+
+ engineVersion + "'.");
84+
}
85+
vaultConfig.setEngineVersion(engineVersion);
86+
this.vaultConfig = vaultConfig;
87+
if (this.vaultConfig.getNameSpace() != null && !this.vaultConfig.getNameSpace().isEmpty()) {
88+
System.out.println(String.format("The Namespace %s has been bound to this Vault instance. Please keep this in mind when running operations.", this.vaultConfig.getNameSpace()));
89+
}
90+
}
91+
92+
/**
93+
* Construct a Vault driver instance with the provided config settings.
94+
*
95+
* @param vaultConfig Configuration settings for Vault interaction (e.g. server address, token, etc)
96+
* If the Secrets engine version path map is not provided, or does not contain the
97+
* requested secret, fall back to the global version supplied.
98+
* @param useSecretsEnginePathMap Whether to use a provided KV Engine version map from the Vault config, or generate one.
99+
* If a secrets KV Engine version map is not supplied, use Vault APIs to determine the
100+
* KV Engine version for each secret. This call requires admin rights.
101+
* @param globalFallbackVersion The Integer version of the KV Engine to use as a global fallback.
102+
*/
103+
public Vault(final VaultConfig vaultConfig, final Boolean useSecretsEnginePathMap, final Integer globalFallbackVersion)
104+
throws VaultException {
105+
this.vaultConfig = vaultConfig;
106+
if (this.vaultConfig.getNameSpace() != null && !this.vaultConfig.getNameSpace().isEmpty()) {
107+
System.out.println(String.format("The Namespace %s has been bound to this Vault instance. Please keep this in mind when running operations.", this.vaultConfig.getNameSpace()));
108+
}
109+
this.vaultConfig.setEngineVersion(globalFallbackVersion);
110+
if (useSecretsEnginePathMap && this.vaultConfig.getSecretsEnginePathMap().isEmpty()) {
111+
try {
112+
System.out.println("No secrets Engine version map was supplied, attempting to generate one.");
113+
final Map<String, String> secretsEnginePathMap = collectSecretEngineVersions();
114+
assert secretsEnginePathMap != null;
115+
this.vaultConfig.getSecretsEnginePathMap().putAll(secretsEnginePathMap);
116+
} catch (Exception e) {
117+
throw new VaultException(String.format("An Engine KV version map was not supplied, and unable to determine " +
118+
"KV Engine " +
119+
"version, " + "due to exception: %s", e.getMessage() + ". Do you have admin rights?"));
120+
}
121+
}
56122
}
57123

58124
/**
59125
* This method is chained ahead of endpoints (e.g. <code>logical()</code>, <code>auth()</code>,
60126
* etc... to specify retry rules for any API operations invoked on that endpoint.
61127
*
62-
* @param maxRetries The number of times that API operations will be retried when a failure occurs
128+
* @param maxRetries The number of times that API operations will be retried when a failure occurs
63129
* @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in between retries
64130
* @return This object, with maxRetries and retryIntervalMilliseconds populated
65131
*/
@@ -146,4 +212,61 @@ public Debug debug() {
146212
public Seal seal() {
147213
return new Seal(vaultConfig);
148214
}
215+
216+
/**
217+
* Makes a REST call to Vault, to collect information on which secret engine version (if any) is used by each available
218+
* mount point. Possibilities are:
219+
*
220+
* <ul>
221+
* <li>"2" - A mount point running on Vault 0.10 or higher, configured to use the engine 2 API</li>
222+
* <li>"1" - A mount point running on Vault 0.10 or higher, configured to use the engine 1 API</li>
223+
* <li>"unknown" - A mount point running on an older version of Vault. Can more or less be treated as "1".</li>
224+
* </ul>
225+
* <p>
226+
* IMPORTANT: Whichever authentication mechanism is being used with the <code>VaultConfig</code> object, that principal
227+
* needs permission to access the <code>/v1/sys/mounts</code> REST endpoint.
228+
*
229+
* @return A map of mount points (e.g. "/secret") to secret engine version numbers (e.g. "2")
230+
*/
231+
private Map<String, String> collectSecretEngineVersions() {
232+
try {
233+
final RestResponse restResponse = new Rest()//NOPMD
234+
.url(vaultConfig.getAddress() + "/v1/sys/mounts")
235+
.header("X-Vault-Token", vaultConfig.getToken())
236+
.optionalHeader("X-Vault-Namespace", this.vaultConfig.getNameSpace())
237+
.connectTimeoutSeconds(vaultConfig.getOpenTimeout())
238+
.readTimeoutSeconds(vaultConfig.getReadTimeout())
239+
.sslVerification(vaultConfig.getSslConfig().isVerify())
240+
.sslContext(vaultConfig.getSslConfig().getSslContext())
241+
.get();
242+
if (restResponse.getStatus() != 200) {
243+
return null;
244+
}
245+
246+
final String jsonString = new String(restResponse.getBody(), StandardCharsets.UTF_8);
247+
final Map<String, String> data = new HashMap<>();
248+
final JsonObject jsonData = Json.parse(jsonString).asObject().get("data").asObject();
249+
for (JsonObject.Member member : jsonData) {
250+
final String name = member.getName();
251+
String version = "unknown";
252+
253+
final JsonValue options = member.getValue().asObject().get("options");
254+
if (options != null && options.isObject()) {
255+
final JsonValue ver = options.asObject().get("version");
256+
if (ver != null && ver.isString()) {
257+
version = ver.asString();
258+
}
259+
}
260+
data.put(name, version);
261+
}
262+
return data;
263+
} catch (RestException e) {
264+
System.err.print(String.format("Unable to retrieve the KV Engine secrets, due to exception: %s", e.getMessage()));
265+
return null;
266+
}
267+
}
268+
269+
public Map<String, String> getSecretEngineVersions() {
270+
return this.collectSecretEngineVersions();
271+
}
149272
}

src/main/java/com/bettercloud/vault/VaultConfig.java

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import lombok.Getter;
44

55
import java.io.Serializable;
6+
import java.util.Map;
7+
import java.util.concurrent.ConcurrentHashMap;
68

79
/**
810
* <p>A container for the configuration settings needed to initialize a <code>Vault</code> driver instance.</p>
@@ -30,13 +32,26 @@ public class VaultConfig implements Serializable {
3032
private static final String VAULT_OPEN_TIMEOUT = "VAULT_OPEN_TIMEOUT";
3133
private static final String VAULT_READ_TIMEOUT = "VAULT_READ_TIMEOUT";
3234

33-
@Getter private String address;
34-
@Getter private String token;
35-
@Getter private SslConfig sslConfig;
36-
@Getter private Integer openTimeout;
37-
@Getter private Integer readTimeout;
38-
@Getter private int maxRetries;
39-
@Getter private int retryIntervalMilliseconds;
35+
@Getter
36+
private Map<String, String> secretsEnginePathMap = new ConcurrentHashMap<>();
37+
@Getter
38+
private String address;
39+
@Getter
40+
private String token;
41+
@Getter
42+
private SslConfig sslConfig;
43+
@Getter
44+
private Integer openTimeout;
45+
@Getter
46+
private Integer readTimeout;
47+
@Getter
48+
private int maxRetries;
49+
@Getter
50+
private int retryIntervalMilliseconds;
51+
@Getter
52+
private Integer globalEngineVersion;
53+
@Getter
54+
private String nameSpace;
4055
private EnvironmentLoader environmentLoader;
4156

4257
/**
@@ -47,10 +62,10 @@ public class VaultConfig implements Serializable {
4762
* constructing a <code>VaultConfig</code> instance using the builder pattern approach rather than the convenience
4863
* constructor. This method's access level was therefore originally set to <code>protected</code>, but was bumped
4964
* up to <code>public</code> due to community request for the ability to disable environment loading altogether
50-
* (see https://github.com/BetterCloud/vault-java-driver/issues/77).
65+
* (see https://github.com/BetterCloud/vault-java-driver/issues/77).</p>
5166
*
52-
* Note that if you do override this, however, then obviously all of the environment checking discussed in the
53-
* documentation becomes disabled.
67+
* <p>Note that if you do override this, however, then obviously all of the environment checking discussed in the
68+
* documentation becomes disabled.</p>
5469
*
5570
* @param environmentLoader An environment variable loader implementation (presumably a mock)
5671
* @return This object, with environmentLoader populated, ready for additional builder-pattern method calls or else finalization with the build() method
@@ -60,6 +75,36 @@ public VaultConfig environmentLoader(final EnvironmentLoader environmentLoader)
6075
return this;
6176
}
6277

78+
/**
79+
* <p>Optional. Sets a global namespace to the Vault server instance, if desired. Otherwise, namespace can be applied individually to any read / write / auth call.
80+
*
81+
* <p>Namespace support requires Vault Enterprise Pro, please see https://learn.hashicorp.com/vault/operations/namespaces</p>
82+
*
83+
* @param nameSpace The namespace to use globally in this VaultConfig instance.
84+
* @return This object, with the namespace populated, ready for additional builder-pattern method calls or else
85+
* finalization with the build() method
86+
*/
87+
public VaultConfig nameSpace(final String nameSpace) throws VaultException {
88+
if (nameSpace != null && !nameSpace.isEmpty()) {
89+
this.nameSpace = nameSpace;
90+
return this;
91+
} else throw new VaultException("A namespace cannot be empty.");
92+
}
93+
94+
/**
95+
* <p>Sets the KV Secrets Engine version of the Vault server instance.
96+
*
97+
* <p>If no version is explicitly set, it will be defaulted to version 2, the current version.</p>
98+
*
99+
* @param globalEngineVersion The Vault KV Secrets Engine version
100+
* @return This object, with KV Secrets Engine version populated, ready for additional builder-pattern method calls or else
101+
* finalization with the build() method
102+
*/
103+
public VaultConfig engineVersion(final Integer globalEngineVersion) {
104+
this.globalEngineVersion = globalEngineVersion;
105+
return this;
106+
}
107+
63108
/**
64109
* <p>Sets the address (URL) of the Vault server instance to which API calls should be sent.
65110
* E.g. <code>http://127.0.0.1:8200</code>.</p>
@@ -95,6 +140,20 @@ public VaultConfig token(final String token) {
95140
return this;
96141
}
97142

143+
/**
144+
* <p>Sets the secrets Engine paths used by Vault.</p>
145+
*
146+
* @param secretEngineVersions paths to use for accessing Vault secrets.
147+
* Key: secret path, value: Engine version to use.
148+
* Example map: "/secret/foo" , "1",
149+
* "/secret/bar", "2"
150+
* @return This object, with secrets paths populated, ready for additional builder-pattern method calls or else finalization with the build() method
151+
*/
152+
VaultConfig secretsEnginePathMap(final Map<String, String> secretEngineVersions) {
153+
this.secretsEnginePathMap = secretEngineVersions;
154+
return this;
155+
}
156+
98157
/**
99158
* <p>A container for SSL-related configuration options (e.g. certificates).</p>
100159
*
@@ -150,7 +209,7 @@ public VaultConfig readTimeout(final Integer readTimeout) {
150209
*
151210
* @param maxRetries The number of times that API operations will be retried when a failure occurs.
152211
*/
153-
protected void setMaxRetries(final int maxRetries) {
212+
void setMaxRetries(final int maxRetries) {
154213
this.maxRetries = maxRetries;
155214
}
156215

@@ -164,10 +223,20 @@ protected void setMaxRetries(final int maxRetries) {
164223
*
165224
* @param retryIntervalMilliseconds The number of milliseconds that the driver will wait in between retries.
166225
*/
167-
protected void setRetryIntervalMilliseconds(final int retryIntervalMilliseconds) {
226+
void setRetryIntervalMilliseconds(final int retryIntervalMilliseconds) {
168227
this.retryIntervalMilliseconds = retryIntervalMilliseconds;
169228
}
170229

230+
/**
231+
* <p>Sets the global Engine version for this Vault Config instance. If no KV Engine version map is provided, use this version
232+
* globally.</p>
233+
* If the provided KV Engine version map does not contain a requested secret, or when writing new secrets, fall back to this version.
234+
*
235+
* @param engineVersion The version of the Vault KV Engine to use globally.
236+
*/
237+
void setEngineVersion(final Integer engineVersion) {
238+
this.globalEngineVersion = engineVersion;
239+
}
171240

172241
/**
173242
* <p>This is the terminating method in the builder pattern. The method that validates all of the fields that

0 commit comments

Comments
 (0)