diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fc8fe83b..39ec971f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,14 +17,6 @@ jobs:
java:
- 11
vault:
- - 1.8.12
- - 1.9.10
- - 1.10.11
- - 1.11.12
- - 1.12.11
- - 1.13.8
- - 1.14.4
- - 1.15.6
- 1.16.3
- 1.17.6
- 1.18.5
@@ -36,6 +28,9 @@ jobs:
- 2.0.2
- 2.0.3
- 2.0.4
+ - 2.1.0
+ - 2.1.1
+ - 2.1.2
- latest
os:
- ubuntu-latest
@@ -64,4 +59,6 @@ jobs:
with:
overwrite: true
name: test-results-${{ matrix.os }}-${{ matrix.vault }}
- path: build/test-results/
+ path: |
+ target/surefire-reports/TEST-*.xml
+ target/failsafe-reports/TEST-*.xml
diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml
index f0eda16e..d208e7e6 100644
--- a/.github/workflows/test-report.yml
+++ b/.github/workflows/test-report.yml
@@ -4,16 +4,19 @@ on:
workflows: [ 'CI' ] # runs after CI workflow
types:
- completed
+permissions:
+ contents: read
+ actions: read # download artifacts of the CI run
+ checks: write # publish the report as a check run
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: dorny/test-reporter@v1
with:
- artifact: test-results
- name: Tests Report
- path: '**/reports/*.*'
+ artifact: /test-results-(.*)/ # one report per CI matrix entry
+ name: Tests Report $1
+ path: '**/TEST-*.xml'
reporter: java-junit
list-suites: 'all'
list-tests: 'all'
-
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1c5058e3..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-language: java
-
-dist: xenial
-
-services:
- - docker
-
-before_cache:
- - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
-
-cache:
- directories:
- - $HOME/.gradle/caches/
- - $HOME/.gradle/wrapper/
-
-matrix:
- include:
- - jdk: openjdk12
- - jdk: openjdk11
- - jdk: openjdk9
-
-script:
- - ./gradlew build integrationTest -s --info
-
-notifications:
- email: false
diff --git a/README.md b/README.md
index 483e2e51..47971243 100644
--- a/README.md
+++ b/README.md
@@ -15,14 +15,12 @@ most commonly used Java clients for Hashicorp, but has had no activity or releas
December 2019. The project is not
maintaining [by author](https://github.com/BetterCloud/vault-java-driver/pull/245#issuecomment-954066376).
-This Change
------------
-
Table of Contents
-----------------
* [Installing the Driver](#installing-the-driver)
* [Initializing a Driver Instance](#initializing-a-driver-instance)
+* [Environment Variables](#environment-variables)
* [Key/Value Secret Engine Config](#key-value-secret-engine-config)
* [SSL Config](#ssl-config)
* [General Options](#general-options)
@@ -85,12 +83,14 @@ final VaultConfig config =
new VaultConfig()
.address("http://127.0.0.1:8200") // Defaults to "VAULT_ADDR" environment variable
.token("3c9fd6be-7bc2-9d1f-6fb3-cd746c0fc4e8") // Defaults to "VAULT_TOKEN" environment variable
- .openTimeout(5) // Defaults to "VAULT_OPEN_TIMEOUT" environment variable
- .readTimeout(30) // Defaults to "VAULT_READ_TIMEOUT" environment variable
+ .openTimeout(5) // Defaults to "VAULT_OPEN_TIMEOUT" environment variable (0 = no timeout)
+ .readTimeout(30) // Defaults to "VAULT_READ_TIMEOUT" environment variable (0 = no timeout)
.sslConfig(new SslConfig().build()) // See "SSL Config" section below
.build();
```
+See [Environment Variables](#environment-variables) for how these variables are read.
+
Once you have initialized a `VaultConfig` object, you can use it to construct an instance of
the `Vault` primary driver class:
@@ -98,6 +98,42 @@ the `Vault` primary driver class:
final Vault vault = Vault.create(config);
```
+Environment Variables
+---------------------
+Any setting that is not set in code is read from an environment variable when `build()` is called.
+A value set in code always takes priority, and the matching environment variable is then not read
+at all.
+
+| Variable | Setting | Notes |
+|----------------------|----------------------------|-------------------------------------------------------------------------------------------------------------------------------|
+| `VAULT_ADDR` | `VaultConfig.address` | Required if no address is set in code, otherwise `build()` fails with "No address is set". |
+| `VAULT_TOKEN` | `VaultConfig.token` | If unset, the token is read from the `.vault-token` file in the user's home directory, if present. |
+| `VAULT_OPEN_TIMEOUT` | `VaultConfig.openTimeout` | Seconds to wait for a connection. `0` means no timeout. |
+| `VAULT_READ_TIMEOUT` | `VaultConfig.readTimeout` | Seconds to wait for a response. `0` means no timeout. |
+| `VAULT_SSL_VERIFY` | `SslConfig.verify` | Whether to verify the Vault server's SSL certificate. Only `false` disables verification (see below). |
+| `VAULT_SSL_CERT` | `SslConfig.pemFile` | Path to a PEM file with the Vault server's certificate. Only used if verification is enabled and no certificate is set in code. |
+
+All values are handled the same way:
+
+* Surrounding whitespace is removed, so a trailing newline (e.g. from a mounted Kubernetes secret)
+ is harmless.
+* An empty or blank value is treated as unset. This also applies to the `.vault-token` file.
+* An invalid timeout (not an integer, or negative) is ignored, and a warning is logged. A negative
+ timeout set in code makes `build()` throw a `VaultException`.
+
+`VAULT_SSL_VERIFY` fails closed, so that a typo can never disable SSL verification by accident:
+
+* `false` (in any case) disables verification.
+* `true`, `t`, `1`, `y`, `yes` and `on` (in any case), or an empty value, enable it.
+* Any other value, including `0`, `no` and `off`, also enables it, and a warning is logged.
+
+To disable loading from environment variables, or to read the values from another source, pass
+your own `EnvironmentLoader` implementation to `VaultConfig.environmentLoader()`. Whitespace removal
+and empty values are still handled as above for the values it returns, while the `.vault-token`
+fallback is part of the default `EnvironmentLoader` only. An `SslConfig` passed to `VaultConfig`
+without calling its own `build()` is built by `VaultConfig.build()`, using the same
+`EnvironmentLoader`.
+
Key Value Secret Engine Config
------------------------------
Shortly before its `1.0` release, Vault added a Version 2 of
@@ -166,10 +202,17 @@ own `SslConfig` class. This class likewise using a builder pattern.
.verify(false) // Defaults to "VAULT_SSL_VERIFY" environment variable (or else "true")
```
-To disable SSL certificate verification altogether, set `sslVerify(false)`. YOU SHOULD NOT DO THIS
+To disable SSL certificate verification altogether, set `verify(false)`, or set the
+`VAULT_SSL_VERIFY` environment variable to `false` (see
+[Environment Variables](#environment-variables)). YOU SHOULD NOT DO THIS
IS A REAL PRODUCTION SETTING! However, it can be useful in a development or testing server context.
If this value is explicitly set to `false`, then all other SSL config is basically unused.
+Certificate settings changed after calling `build()` only take effect once `build()` is called
+again. An `SslConfig` can be serialized together with its PEM data, but not with its JKS keystore
+and truststore: after deserialization, a JKS-based config verifies the Vault server's certificate
+against the JVM's default trusted CAs instead.
+
#### Java Keystore (JKS) based config
You can provide the driver with a JKS truststore, containing Vault's server-side certificate for
@@ -221,7 +264,7 @@ the `SslConfig` Javadocs for more detail).
If SSL verification is enabled, no JKS-based config is provided, AND none of these three methods are
called, then `SslConfig` will by default check for a `VAULT_SSL_CERT` environment variable. If
-that's sets then it will be treated as a filesystem path.
+that's set, then it will be treated as a filesystem path.
To use Vault's TLS Certificate auth backend for SSL client auth, you must provide your client
certificate and private key, using some pair from the following options:
@@ -293,6 +336,10 @@ final LogicalResponse response = vault.withRetries(5, 1000)
.read("secret/hello");
```
+Only transient failures are retried: I/O errors (e.g. connection refused or timed out) and HTTP 5xx,
+408, 412 and 429 responses. Other 4xx responses (e.g. 400, 403, 404) fail immediately, since
+retrying them would not help.
+
API Reference (Javadocs)
------------------------
Full [Javadoc documentation](https://jopenlibs.github.io/vault-java-driver/javadoc).
@@ -305,6 +352,61 @@ and may require modifications in your code to migrate. Changes to the minor vers
number) should represent non-breaking changes. The third number represents any very minor bugfix
patches.
+* **Unreleased**: This release contains the following updates:
+ * Security: fixes [CVE-2026-109408](https://www.cve.org/CVERecord?id=CVE-2026-109408), where SSL
+ certificate verification could be silently disabled. See the `VAULT_SSL_VERIFY` and `SslConfig`
+ entries below
+ * Environment variables: surrounding whitespace is removed, and empty values are treated as unset
+ * `VAULT_SSL_VERIFY` now only disables SSL verification when set to `false`. Previously any value
+ other than `true` disabled it, including `1`, `yes`, typos and empty values. **Values like `0`,
+ `no` or `off` now keep verification enabled: use `false` instead.**
+ * A timeout of `0` means no timeout again, as before 6.0.0. Negative timeouts are rejected
+ * `VAULT_TOKEN` and the `.vault-token` file are no longer read when a token is set in code
+ * An `SslConfig` passed to `VaultConfig` without calling its `build()` is now built by
+ `VaultConfig.build()`, and verifies SSL certificates by default. Previously it silently disabled
+ verification and ignored its certificates
+ * A deserialized `SslConfig` now keeps its settings. Previously it had SSL verification disabled.
+ An `SslConfig` serialized by an older release can still be deserialized, and verifies SSL
+ certificates unless it was serialized with `verify(false)`. This includes one built with
+ `VAULT_SSL_VERIFY=false`, since the serialized data cannot tell where that value came from
+ * Each `VaultConfig` now reuses a single HTTP client, so connections are reused between requests
+ instead of a new client being created for every request. `VaultConfig.getHttpClient()` now
+ returns the client in use instead of `null` when none was set. The client is rebuilt when the
+ `SslConfig` changes (e.g. SSL verification turned on or off, or `SslConfig.build()` called again
+ with new certificates)
+ * A `VaultConfig` with a custom `HttpClient` can be serialized. The client itself is not
+ serialized: the deserialized copy uses its own default client
+ * Query parameters are url-encoded only once. Previously reserved characters (e.g. `/`, `&`, `%`)
+ were encoded twice
+ * Response bodies are returned exactly as received. Previously non-UTF-8 (e.g. binary) bodies
+ were corrupted
+ * Only transient errors are retried: I/O errors and HTTP 5xx, 408, 412 and 429 responses.
+ **Other 4xx responses (e.g. 400, 403, 404) now fail immediately instead of being retried.** An
+ interrupt while waiting between retries stops retrying
+ * `AuthResponse`, `WrapResponse`, `LookupResponse` and `UnwrapResponse` no longer throw on
+ missing or `null` JSON fields
+ * `Vault.create(config, null)` throws `IllegalArgumentException`, and a `null`
+ `useSecretsEnginePathMap` counts as `false`. When no KV engine version is set, version 2 is used
+ (`Logical.getEngineVersionForSecretPath()` returns `2` instead of `null`)
+ * `VaultConfig.nameSpace()` no longer throws `VaultException`: an empty namespace is rejected by
+ `build()` instead. `VaultConfig.getSecretsEnginePathMap()` returns an unmodifiable map
+ * `VaultConfig.address()` treats a `null` or blank address as unset, falling back to `VAULT_ADDR`,
+ instead of throwing `NullPointerException`
+ * When the KV engine version map cannot be generated (e.g. no access to `sys/mounts`), the
+ `VaultException` message says so, instead of ending in `null`
+ * Warnings are logged with `java.util.logging` instead of being printed to standard error
+ * Build: pinned Maven plugin versions, fixed test reports in the GitHub Actions CI, removed the
+ unused Travis CI configuration. Checkstyle now runs in the build, and fails it on violations
+ * CI tests Vault 1.16 to 2.1 only: versions 1.8 to 1.15 are deprecated. `Auth.loginByAppID()` is
+ deprecated, since the App ID auth backend is not available in Vault 1.16 and later: use
+ `Auth.loginByAppRole()` instead
+ * Methods documented as deprecated are now also annotated with `@Deprecated`, so using them
+ produces a compiler warning: `Vault.leases()`, `Vault.mounts()`, `Vault.seal()`,
+ `Auth.lookupWrap()`, `Auth.unwrap()`, `Auth.wrap()` and `Auth.rewrap()`. Use their `Sys`
+ equivalents instead (e.g. `vault.sys().leases()`, `vault.sys().wrapping().unwrap()`)
+ * Javadoc examples use the `Sys` methods instead of the deprecated ones. The `Wrapping` examples
+ are fixed: they used `new Vault(config)` instead of `Vault.create(config)`, and a misspelled
+ `lookupWarp()` method
* **6.2.3**: This release contains the following updates:
* Improved Java 11 compatibility
* Fixed integration tests with Vault 2.x
diff --git a/pom.xml b/pom.xml
index 903b8889..ece485af 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,7 +111,7 @@
If The code used to load environment variables is encapsulated here, so that a mock version
@@ -90,7 +96,8 @@ protected SslConfig environmentLoader(final EnvironmentLoader environmentLoader)
*
* If no verify is explicitly set, either by this method in a builder pattern approach or
* else by one of the convenience constructors, then VAULT_TOKEN is unset, it is read from the .vault-token file
+ * in the executing user's home directory, if any.null if it is unset
+ */
public String loadVariable(final String name) {
- String value = null;
- if (VaultConfig.VAULT_TOKEN.equals(name)) {
-
- // Special handling for the VAULT_TOKEN variable, since it can be read from the filesystem if it's not
- // found in the environment
- if (System.getenv(VaultConfig.VAULT_TOKEN) != null) {
- // Found it in the environment
- value = System.getenv(name);
- } else {
- // Not in the environment, looking for a ".vault-token" file in the executing user's home directory instead
- try {
- value = Files.readString(
- Paths.get(System.getProperty("user.home")).resolve(".vault-token")).strip();
- } catch (IOException e) {
- // No-op... there simply isn't a token value available
- }
+ String value = normalize(getenv(name));
+ if (value == null && VaultConfig.VAULT_TOKEN.equals(name)) {
+ // Not in the environment, looking for a ".vault-token" file in the executing user's home directory instead
+ try {
+ value = normalize(Files.readString(userHome().resolve(".vault-token")));
+ } catch (IOException e) {
+ // No-op... there simply isn't a token value available
}
- } else {
-
- // Normal handling for all other variables. We just check the environment.
- value = System.getenv(name);
}
return value;
}
+ /**
+ * Loads an environment variable with the given loader, removing surrounding whitespace and
+ * treating an empty or blank value as unset. This is done here too, because
+ * {@link #loadVariable(String)} may be overridden.
+ *
+ * @param loader The environment loader to use
+ * @param name The environment variable name
+ * @return The variable value without surrounding whitespace, or null if it is
+ * unset, empty or blank
+ */
+ static String load(final EnvironmentLoader loader, final String name) {
+ return normalize(loader.loadVariable(name));
+ }
+
+ /**
+ * Reads a variable from the process environment. Overridden by unit tests.
+ */
+ String getenv(final String name) {
+ return System.getenv(name);
+ }
+
+ /**
+ * @return The executing user's home directory. Overridden by unit tests.
+ */
+ Path userHome() {
+ return Paths.get(System.getProperty("user.home"));
+ }
+
+ private static String normalize(final String value) {
+ return value == null || value.isBlank() ? null : value.strip();
+ }
+
}
diff --git a/src/main/java/io/github/jopenlibs/vault/SslConfig.java b/src/main/java/io/github/jopenlibs/vault/SslConfig.java
index b60b24b1..11d8fd05 100644
--- a/src/main/java/io/github/jopenlibs/vault/SslConfig.java
+++ b/src/main/java/io/github/jopenlibs/vault/SslConfig.java
@@ -27,6 +27,9 @@
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
+import java.util.List;
+import java.util.Locale;
+import java.util.logging.Logger;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
@@ -46,8 +49,10 @@ public class SslConfig implements Serializable {
private static final String VAULT_SSL_VERIFY = "VAULT_SSL_VERIFY";
private static final String VAULT_SSL_CERT = "VAULT_SSL_CERT";
+ private static final ListSslConfig will look to the
- * VAULT_SSL_VERIFY environment variable.VAULT_SSL_VERIFY environment variable. Only the value false
+ * (case-insensitive) disables verification; any other value keeps it enabled.
true.
@@ -495,27 +502,87 @@ public SslConfig build() throws VaultException {
if (this.environmentLoader == null) {
this.environmentLoader = new EnvironmentLoader();
}
- if (this.verifyObject == null && environmentLoader.loadVariable(VAULT_SSL_VERIFY) != null) {
- this.verify = Boolean.parseBoolean(environmentLoader.loadVariable(VAULT_SSL_VERIFY));
- } else if (this.verifyObject != null) {
+ if (this.verifyObject != null) {
this.verify = verifyObject;
} else {
- this.verify = true;
+ this.verify = parseVerify(EnvironmentLoader.load(environmentLoader, VAULT_SSL_VERIFY));
}
- if (this.verify && this.pemUTF8 == null
- && environmentLoader.loadVariable(VAULT_SSL_CERT) != null) {
- final var pemFile = new File(environmentLoader.loadVariable(VAULT_SSL_CERT));
- try {
- this.pemUTF8 = Files.readString(pemFile.toPath(), StandardCharsets.UTF_8);
- } catch (IOException e) {
- throw new VaultException(e);
+ if (this.verify && this.pemUTF8 == null) {
+ final String pemPath = EnvironmentLoader.load(environmentLoader, VAULT_SSL_CERT);
+ if (pemPath != null) {
+ try {
+ this.pemUTF8 = Files.readString(new File(pemPath).toPath(),
+ StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new VaultException(e);
+ }
}
}
buildSsl();
+ this.built = true;
return this;
}
+ /**
+ * Builds this object, unless {@link #build()} was already called. Used by
+ * {@link VaultConfig#build()}, so that a SslConfig passed to it is ready for use
+ * even if its own build() was not called.
+ *
+ * @param defaultEnvironmentLoader The environment loader to use, if none was set on this object
+ * @return This object
+ * @throws VaultException If any problem occurs while building
+ */
+ SslConfig buildIfNeeded(final EnvironmentLoader defaultEnvironmentLoader)
+ throws VaultException {
+ if (built) {
+ return this;
+ }
+ if (this.environmentLoader == null) {
+ this.environmentLoader = defaultEnvironmentLoader;
+ }
+ return build();
+ }
+
+ /**
+ * Parses the VAULT_SSL_VERIFY environment variable, ignoring case and surrounding
+ * whitespace. Verification is only disabled by an explicit false, so that a typo
+ * or an unexpected value can never silently turn it off. t, true,
+ * 1, y, yes and on enable it; any other
+ * value also enables it, with a warning.
+ *
+ * @param value The variable value, or null if unset
+ * @return false only if the value is false
+ */
+ static boolean parseVerify(final String value) {
+ if (value == null || value.isBlank()) {
+ return true;
+ }
+ final String normalized = value.strip().toLowerCase(Locale.ROOT);
+ if (normalized.equals("false")) {
+ return false;
+ }
+ if (!VERIFY_TRUE_VALUES.contains(normalized)) {
+ LOGGER.warning(String.format(
+ "The %s environment variable contains value \"%s\", which is neither \"false\" nor one of %s. SSL verification stays enabled.",
+ VAULT_SSL_VERIFY, value, VERIFY_TRUE_VALUES));
+ }
+ return true;
+ }
+
+ /**
+ * Whether HTTPS connections should verify the Vault server's SSL certificate.
+ * + *A value set with {@link #verify(Boolean)} always takes priority, even if it was set after
+ * {@link #build()}. Otherwise this is the value resolved by {@link #build()} (from the
+ * VAULT_SSL_VERIFY environment variable), or true if
+ * {@link #build()} was not called.
false only if verification was explicitly disabled
+ */
public boolean isVerify() {
+ if (verifyObject != null) {
+ return verifyObject;
+ }
return verify;
}
@@ -721,6 +788,14 @@ private static String inputStreamToUTF8(final InputStream input) throws IOExcept
*/
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {//NOPMD
+ // Restore the serialized fields first: buildSsl() depends on them (e.g. verify, pemUTF8)
+ in.defaultReadObject();
+ // Field initializers don't run on deserialization, so a stream written by an older release
+ // (which has no "built" field) or by a never-built instance may carry verify=false without
+ // it ever being requested. Unless explicitly disabled, verification stays enabled.
+ if (!built && verifyObject == null) {
+ this.verify = true;
+ }
try {
buildSsl();
} catch (VaultException e) {
diff --git a/src/main/java/io/github/jopenlibs/vault/Vault.java b/src/main/java/io/github/jopenlibs/vault/Vault.java
index 77ef1894..dffb117f 100644
--- a/src/main/java/io/github/jopenlibs/vault/Vault.java
+++ b/src/main/java/io/github/jopenlibs/vault/Vault.java
@@ -124,6 +124,7 @@ static Vault create(final VaultConfig vaultConfig, final Boolean useSecretsEngin
* @see Sys#leases()
* @deprecated This method is deprecated and in future it will be removed
*/
+ @Deprecated
Leases leases();
/**
@@ -137,12 +138,14 @@ static Vault create(final VaultConfig vaultConfig, final Boolean useSecretsEngin
* @see Sys#mounts()
* @deprecated This method is deprecated and in future it will be removed
*/
+ @Deprecated
Mounts mounts();
/**
* @see Sys#seal()
* @deprecated This method is deprecated and in future it will be removed
*/
+ @Deprecated
Seal seal();
MapA container for the configuration settings needed to initialize a The code used to load environment variables is encapsulated here, so that a mock version
@@ -113,12 +121,18 @@ public VaultConfig engineVersion(final Integer globalEngineVersion) {
* it explicitly AND no environment variable value is found, then initialization of the
* A Vault driver
@@ -33,6 +37,8 @@ public class VaultConfig implements Serializable {
private static final String VAULT_OPEN_TIMEOUT = "VAULT_OPEN_TIMEOUT";
private static final String VAULT_READ_TIMEOUT = "VAULT_READ_TIMEOUT";
+ private static final Logger LOGGER = Logger.getLogger(VaultConfig.class.getCanonicalName());
+
private MapVaultConfig object will fail.null, empty or blank address is treated as unset.SslConfig object with {@link SslConfig#verify(Boolean)} explicitly set to
* false.
If {@link SslConfig#build()} was not called on the passed object, {@link #build()} calls
+ * it, using this object's environment loader unless the SslConfig has its
+ * own.
0 for no timeout
* @return This object, with openTimeout populated, ready for additional builder-pattern method
* calls or else finalization with the build() method
*/
@@ -224,7 +242,7 @@ public VaultConfig openTimeout(final Integer openTimeout) {
* environment variable.
*
* @param readTimeout Number of seconds to wait for all data to be retrieved from an established
- * HTTP(S) connection
+ * HTTP(S) connection, or 0 for no timeout
* @return This object, with readTimeout populated, ready for additional builder-pattern method
* calls or else finalization with the build() method
*/
@@ -243,7 +261,8 @@ public VaultConfig readTimeout(final Integer readTimeout) {
*/
public VaultConfig prefixPathDepth(int prefixPathDepth) {
if (prefixPathDepth < 1) {
- throw new IllegalArgumentException("pathLength must be > 1");
+ throw new IllegalArgumentException(
+ "prefixPathDepth must be at least 1, was: " + prefixPathDepth);
}
this.prefixPathDepth = prefixPathDepth;
@@ -286,8 +305,11 @@ public VaultConfig prefixPath(String prefixPath) {
/**
* Set a preconfigured HttpClient instance to use by REST API calls. This allows to reuse - * http resources (connections, worker threads) between calls. If a preconfigured HttpClient is specified, then - * sslConfig and openTimeout values passed to VaultConfig are ignored. + * http resources (connections, worker threads) between calls. If a preconfigured HttpClient is + * specified, then sslConfig and openTimeout values passed to VaultConfig are ignored. + * + *
The client is not serialized: a deserialized VaultConfig builds its own
+ * client from sslConfig and openTimeout, unless a new one is set with this method.
address field was left unset, and there is no
- * VAULT_ADDR environment variable value with which to populate it.
+ * VAULT_ADDR environment variable value with which to populate it, or if a
+ * negative timeout was set.
*/
public VaultConfig build() throws VaultException {
if (this.nameSpace != null && this.nameSpace.isEmpty()) {
@@ -354,45 +377,65 @@ public VaultConfig build() throws VaultException {
this.environmentLoader = new EnvironmentLoader();
}
if (this.address == null) {
- final String addressFromEnv = environmentLoader.loadVariable(VAULT_ADDR);
+ final String addressFromEnv = EnvironmentLoader.load(environmentLoader, VAULT_ADDR);
if (addressFromEnv != null) {
this.address = addressFromEnv;
} else {
throw new VaultException("No address is set");
}
}
- String envVaultToken = environmentLoader.loadVariable(VAULT_TOKEN);
- if (this.token == null && envVaultToken != null) {
- this.token = envVaultToken.toCharArray();
- }
- if (this.openTimeout == null
- && environmentLoader.loadVariable(VAULT_OPEN_TIMEOUT) != null) {
- try {
- this.openTimeout = Integer.valueOf(
- environmentLoader.loadVariable(VAULT_OPEN_TIMEOUT));
- } catch (NumberFormatException e) {
- System.err.printf("The " + VAULT_OPEN_TIMEOUT
- + " environment variable contains value \"%s\", which cannot be parsed as an integer timeout period.%n",
- environmentLoader.loadVariable(VAULT_OPEN_TIMEOUT));
+ if (this.token == null) {
+ // Only looked up when needed: the default loader may read ~/.vault-token from disk
+ final String envVaultToken = EnvironmentLoader.load(environmentLoader, VAULT_TOKEN);
+ if (envVaultToken != null) {
+ this.token = envVaultToken.toCharArray();
}
}
- if (this.readTimeout == null
- && environmentLoader.loadVariable(VAULT_READ_TIMEOUT) != null) {
- try {
- this.readTimeout = Integer.valueOf(
- environmentLoader.loadVariable(VAULT_READ_TIMEOUT));
- } catch (NumberFormatException e) {
- System.err.printf("The " + VAULT_READ_TIMEOUT
- + " environment variable contains value \"%s\", which cannot be parsed as an integer timeout period.%n",
- environmentLoader.loadVariable(VAULT_READ_TIMEOUT));
- }
+ if (this.openTimeout == null) {
+ this.openTimeout = loadTimeoutVariable(VAULT_OPEN_TIMEOUT);
+ }
+ if (this.readTimeout == null) {
+ this.readTimeout = loadTimeoutVariable(VAULT_READ_TIMEOUT);
+ }
+ if (this.openTimeout != null && this.openTimeout < 0) {
+ throw new VaultException("The open timeout cannot be negative: " + this.openTimeout);
+ }
+ if (this.readTimeout != null && this.readTimeout < 0) {
+ throw new VaultException("The read timeout cannot be negative: " + this.readTimeout);
}
if (this.sslConfig == null) {
- this.sslConfig = new SslConfig().environmentLoader(this.environmentLoader).build();
+ this.sslConfig = new SslConfig();
}
+ this.sslConfig.buildIfNeeded(this.environmentLoader);
return this;
}
+ /**
+ * Reads a timeout period from an environment variable.
+ *
+ * @param name The environment variable name
+ * @return The timeout, or null if the variable is unset, not an integer, or
+ * negative
+ */
+ private Integer loadTimeoutVariable(final String name) {
+ final String value = EnvironmentLoader.load(environmentLoader, name);
+ if (value == null) {
+ return null;
+ }
+ try {
+ final int timeout = Integer.parseInt(value);
+ if (timeout >= 0) {
+ return timeout;
+ }
+ } catch (NumberFormatException e) {
+ // Reported below
+ }
+ LOGGER.warning(String.format(
+ "The %s environment variable contains value \"%s\", which is not a valid timeout period (a non-negative integer).",
+ name, value));
+ return null;
+ }
+
public MapReturns the HttpClient used by REST API calls: the one set with + * {@link #httpClient(HttpClient)} if any, otherwise a client built from this config's sslConfig + * and openTimeout values. The latter is shared by all calls, so connections and worker threads + * are reused, and is rebuilt whenever those values change (e.g. a new sslConfig, SSL + * verification turned on or off, or a new SSLContext from calling {@link SslConfig#build()} + * again).
+ * + *Certificate settings changed on the sslConfig (e.g. {@link SslConfig#pemUTF8(String)}) + * only take effect once {@link SslConfig#build()} is called again.
+ * + * @return The HttpClient used by REST API calls + */ public HttpClient getHttpClient() { - return httpClient; + if (httpClient != null) { + return httpClient; + } + final Boolean verify = sslConfig == null ? null : sslConfig.isVerify(); + final SSLContext sslContext = sslConfig == null ? null : sslConfig.getSslContext(); + var client = defaultHttpClient; + if (client == null || !client.matches(openTimeout, verify, sslContext)) { + synchronized (this) { + client = defaultHttpClient; + if (client == null || !client.matches(openTimeout, verify, sslContext)) { + client = new DefaultHttpClient(openTimeout, verify, sslContext); + defaultHttpClient = client; + } + } + } + return client.httpClient; + } + + /** + * The default HttpClient, along with the settings it was built from. + */ + private static final class DefaultHttpClient { + + private final HttpClient httpClient; + private final Integer openTimeout; + private final Boolean verify; + private final SSLContext sslContext; + + private DefaultHttpClient(final Integer openTimeout, final Boolean verify, + final SSLContext sslContext) { + this.httpClient = Rest.newHttpClient(openTimeout, verify, sslContext); + this.openTimeout = openTimeout; + this.verify = verify; + this.sslContext = sslContext; + } + + private boolean matches(final Integer openTimeout, final Boolean verify, + final SSLContext sslContext) { + // SSLContext has no equals(): a new instance means new certificates + return Objects.equals(this.openTimeout, openTimeout) + && Objects.equals(this.verify, verify) + && this.sslContext == sslContext; + } } } diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 9b824655..04b81768 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -10,11 +10,9 @@ import io.github.jopenlibs.vault.api.sys.Sys; import io.github.jopenlibs.vault.api.sys.mounts.Mounts; import io.github.jopenlibs.vault.json.Json; -import io.github.jopenlibs.vault.json.JsonObject; import io.github.jopenlibs.vault.json.JsonValue; import io.github.jopenlibs.vault.rest.Rest; import io.github.jopenlibs.vault.rest.RestException; -import io.github.jopenlibs.vault.rest.RestResponse; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -71,11 +69,7 @@ public class VaultImpl implements Vault { */ public VaultImpl(final VaultConfig vaultConfig) { this.vaultConfig = vaultConfig; - if (this.vaultConfig.getNameSpace() != null && !this.vaultConfig.getNameSpace().isEmpty()) { - logger.info(String.format( - "The NameSpace %s has been bound to this Vault instance. Please keep this in mind when running operations.", - this.vaultConfig.getNameSpace())); - } + logNameSpace(); if (this.vaultConfig.getSecretsEnginePathMap().isEmpty() && this.vaultConfig.getGlobalEngineVersion() == null) { logger.info( @@ -94,18 +88,14 @@ public VaultImpl(final VaultConfig vaultConfig) { * 2) */ public VaultImpl(final VaultConfig vaultConfig, final Integer engineVersion) { - if (engineVersion < 1 || engineVersion > 2) { + if (engineVersion == null || engineVersion < 1 || engineVersion > 2) { throw new IllegalArgumentException( "The Engine version must be '1' or '2', the version supplied was: '" + engineVersion + "'."); } vaultConfig.setEngineVersion(engineVersion); this.vaultConfig = vaultConfig; - if (this.vaultConfig.getNameSpace() != null && !this.vaultConfig.getNameSpace().isEmpty()) { - logger.info(String.format( - "The Namespace %s has been bound to this Vault instance. Please keep this in mind when running operations.", - this.vaultConfig.getNameSpace())); - } + logNameSpace(); } /** @@ -125,26 +115,36 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa final Integer globalFallbackVersion) throws VaultException { this.vaultConfig = vaultConfig; - if (this.vaultConfig.getNameSpace() != null && !this.vaultConfig.getNameSpace().isEmpty()) { - logger.info(String.format( - "The Namespace %s has been bound to this Vault instance. Please keep this in mind when running operations.", - this.vaultConfig.getNameSpace())); - } + logNameSpace(); this.vaultConfig.setEngineVersion(globalFallbackVersion); - if (useSecretsEnginePathMap && this.vaultConfig.getSecretsEnginePathMap().isEmpty()) { + if (Boolean.TRUE.equals(useSecretsEnginePathMap) && this.vaultConfig.getSecretsEnginePathMap().isEmpty()) { + logger.info( + "No secrets Engine version map was supplied, attempting to generate one."); + final Map/v1/sys/mounts/* REST endpoints).
*
* @return the implementing class for Vault's sys mounts operations
+ *
+ * @deprecated This method is deprecated and in future it will be removed
*/
@Override
+ @Deprecated
public Mounts mounts() {
return new Mounts(vaultConfig);
}
@@ -280,8 +286,11 @@ public Mounts mounts() {
* Returns the implementing class for Vault's seal operations (e.g. seal, unseal, sealStatus).
*
* @return The implementing class for Vault's seal operations (e.g. seal, unseal, sealStatus).
+ *
+ * @deprecated This method is deprecated and in future it will be removed
*/
@Override
+ @Deprecated
public Seal seal() {
return new Seal(vaultConfig);
}
@@ -307,15 +316,12 @@ public Seal seal() {
*/
private Mapsecret/hello).
- * @return the detected engine version (1 or 2), or the global default if not found
+ * @return the detected engine version (1 or 2), or the global default if not found, or 2 if
+ * no global default is set
*/
private Integer engineVersionForSecretPath(final String secretPath) {
final var pathMap = this.config.getSecretsEnginePathMap();
@@ -648,7 +624,7 @@ private Integer engineVersionForSecretPath(final String secretPath) {
idx = prefix.lastIndexOf('/');
} while (idx != -1);
}
- return this.config.getGlobalEngineVersion();
+ return Objects.requireNonNullElse(this.config.getGlobalEngineVersion(), 2);
}
/**
diff --git a/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java b/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java
index 17ec8d7d..5a0b9f66 100644
--- a/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java
+++ b/src/main/java/io/github/jopenlibs/vault/api/OperationsBase.java
@@ -3,6 +3,8 @@
import io.github.jopenlibs.vault.VaultConfig;
import io.github.jopenlibs.vault.VaultException;
import io.github.jopenlibs.vault.rest.Rest;
+import io.github.jopenlibs.vault.rest.RestException;
+import java.io.IOException;
/**
@@ -21,7 +23,9 @@ protected retryCount times when it fails with a
+ * transient error (see {@link #isRetryable(Exception)}). Any other error, or an interrupt
+ * while waiting between attempts, ends the retries and the last error is thrown.
*/
static false if the thread was interrupted while sleeping, in which case the
+ * interrupt flag is restored
+ */
+ private static boolean sleep(long delay) {
try {
Thread.sleep(delay);
+ return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
+ return false;
}
}
diff --git a/src/main/java/io/github/jopenlibs/vault/api/database/Database.java b/src/main/java/io/github/jopenlibs/vault/api/database/Database.java
index f7188d96..b58fee17 100644
--- a/src/main/java/io/github/jopenlibs/vault/api/database/Database.java
+++ b/src/main/java/io/github/jopenlibs/vault/api/database/Database.java
@@ -52,9 +52,7 @@ public Database(final VaultConfig config, final String mountPath) {
super(config);
this.mountPath = mountPath;
- if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
- this.nameSpace = this.config.getNameSpace();
- }
+ this.nameSpace = this.config.getNameSpace();
}
/**
@@ -97,10 +95,7 @@ public DatabaseResponse createOrUpdateRole(final String roleName,
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.body(requestJson.getBytes(StandardCharsets.UTF_8))
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate restResponse
@@ -142,10 +137,7 @@ public DatabaseResponse getRole(final String roleName) throws VaultException {
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.get();
// Validate response
@@ -194,11 +186,8 @@ public DatabaseResponse revoke(final String serialNumber) throws VaultException
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
.body(requestJson.getBytes(StandardCharsets.UTF_8))
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate response
@@ -240,10 +229,7 @@ public DatabaseResponse deleteRole(final String roleName) throws VaultException
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.delete();
// Validate response
@@ -287,10 +273,7 @@ public DatabaseResponse creds(final String roleName) throws VaultException {
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.get();
// Validate response
diff --git a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java
index 6da24214..1a3148e8 100644
--- a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java
+++ b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java
@@ -53,9 +53,7 @@ public Pki(final VaultConfig config, final String mountPath) {
super(config);
this.mountPath = mountPath;
- if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
- this.nameSpace = this.config.getNameSpace();
- }
+ this.nameSpace = this.config.getNameSpace();
}
/**
@@ -123,10 +121,7 @@ public PkiResponse createOrUpdateRole(final String roleName, final RoleOptions o
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.body(requestJson.getBytes(StandardCharsets.UTF_8))
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate restResponse
@@ -170,10 +165,7 @@ public PkiResponse getRole(final String roleName) throws VaultException {
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.get();
// Validate response
@@ -221,11 +213,8 @@ public PkiResponse revoke(final String serialNumber) throws VaultException {
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
.body(requestJson.getBytes(StandardCharsets.UTF_8))
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate response
@@ -267,10 +256,7 @@ public PkiResponse deleteRole(final String roleName) throws VaultException {
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.delete();
// Validate response
@@ -531,10 +517,7 @@ public PkiResponse issue(
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.body(requestJson.getBytes(StandardCharsets.UTF_8))
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate response
diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java
index 7895becd..10938922 100644
--- a/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java
+++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java
@@ -5,8 +5,6 @@
import io.github.jopenlibs.vault.api.OperationsBase;
import io.github.jopenlibs.vault.json.Json;
import io.github.jopenlibs.vault.response.VaultResponse;
-import io.github.jopenlibs.vault.rest.Rest;
-import io.github.jopenlibs.vault.rest.RestResponse;
import java.nio.charset.StandardCharsets;
@@ -27,9 +25,7 @@ public class Leases extends OperationsBase {
public Leases(final VaultConfig config) {
super(config);
- if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
- this.nameSpace = this.config.getNameSpace();
- }
+ this.nameSpace = this.config.getNameSpace();
}
public Leases withNameSpace(final String nameSpace) {
@@ -42,7 +38,7 @@ public Leases withNameSpace(final String nameSpace) {
*
*
* {@code
- * final VaultResponse response = vault.leases().revoke("7c63da27-a56b-3e3b-377d-ef74630a6d0b");
+ * final VaultResponse response = vault.sys().leases().revoke("7c63da27-a56b-3e3b-377d-ef74630a6d0b");
* assertEquals(204, response.getRestResponse().getStatus());
* }
*
@@ -54,16 +50,13 @@ public Leases withNameSpace(final String nameSpace) {
public VaultResponse revoke(final String leaseId) throws VaultException {
return retry(attempt -> {
final String requestJson = Json.object().add("lease_id", leaseId).toString();
- final var restResponse = new Rest()//NOPMD
+ final var restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/sys/leases/revoke")
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.body(requestJson.getBytes(StandardCharsets.UTF_8))
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate response
@@ -84,7 +77,7 @@ public VaultResponse revoke(final String leaseId) throws VaultException {
*
*
* {@code
- * final VaultResponse response = vault.leases().revokePrefix("aws");
+ * final VaultResponse response = vault.sys().leases().revokePrefix("aws");
* assertEquals(204, response.getRestResponse().getStatus());
* }
*
@@ -95,15 +88,12 @@ public VaultResponse revoke(final String leaseId) throws VaultException {
*/
public VaultResponse revokePrefix(final String prefix) throws VaultException {
return retry(attempt -> {
- final var restResponse = new Rest()//NOPMD
+ final var restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/sys/leases/revoke-prefix/" + prefix)
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate response
@@ -126,7 +116,7 @@ public VaultResponse revokePrefix(final String prefix) throws VaultException {
*
*
* {@code
- * final VaultResponse response = vault.leases().revokePrefix("aws");
+ * final VaultResponse response = vault.sys().leases().revokePrefix("aws");
* assertEquals(204, response.getRestResponse().getStatus());
* }
*
@@ -137,15 +127,12 @@ public VaultResponse revokePrefix(final String prefix) throws VaultException {
*/
public VaultResponse revokeForce(final String prefix) throws VaultException {
return retry(attempt -> {
- final var restResponse = new Rest()//NOPMD
+ final var restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/sys/leases/revoke-force/" + prefix)
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate response
@@ -163,7 +150,7 @@ public VaultResponse revokeForce(final String prefix) throws VaultException {
*
*
* {@code
- * final VaultResponse response = vault.leases().renew("mongodb/creds/myapp/cd7f9834-b870-9ebc-3da5-27bf9cdc42ad");
+ * final VaultResponse response = vault.sys().leases().renew("mongodb/creds/myapp/cd7f9834-b870-9ebc-3da5-27bf9cdc42ad");
* assertEquals(200, response.getRestResponse().getStatus());
* }
*
@@ -185,16 +172,13 @@ public VaultResponse renew(final String leaseId, final long increment) throws Va
return retry(attempt -> {
final String requestJson = Json.object().add("increment", increment).toString();
- final var restResponse = new Rest()//NOPMD
+ final var restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/sys/leases/renew/" + leaseId)
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.body(increment < 0 ? null : requestJson.getBytes(StandardCharsets.UTF_8))
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate response
diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java
index 3b8b0226..d4bd8e02 100644
--- a/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java
+++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Seal.java
@@ -5,7 +5,6 @@
import io.github.jopenlibs.vault.api.OperationsBase;
import io.github.jopenlibs.vault.json.Json;
import io.github.jopenlibs.vault.response.SealResponse;
-import io.github.jopenlibs.vault.rest.Rest;
import io.github.jopenlibs.vault.rest.RestResponse;
import java.nio.charset.StandardCharsets;
@@ -26,9 +25,7 @@ public class Seal extends OperationsBase {
public Seal(final VaultConfig config) {
super(config);
- if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
- this.nameSpace = this.config.getNameSpace();
- }
+ this.nameSpace = this.config.getNameSpace();
}
public Seal withNameSpace(final String nameSpace) {
@@ -45,15 +42,12 @@ public Seal withNameSpace(final String nameSpace) {
public SealResponse seal() throws VaultException {
return retry((attempt) -> {
// HTTP request to Vault
- final var restResponse = new Rest()//NOPMD
+ final var restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/sys/seal")
.token(config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
return getSealResponse(attempt, restResponse, 204);
@@ -86,15 +80,12 @@ public SealResponse unseal(final String key, final Boolean reset) throws VaultEx
// HTTP request to Vault
final String requestJson = Json.object().add("key", key).add("reset", reset)
.toString();
- final var restResponse = new Rest()//NOPMD
+ final var restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/sys/unseal")
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.body(requestJson.getBytes(StandardCharsets.UTF_8))
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.post();
// Validate restResponse
@@ -111,14 +102,11 @@ public SealResponse unseal(final String key, final Boolean reset) throws VaultEx
public SealResponse sealStatus() throws VaultException {
return retry((attempt) -> {
// HTTP request to Vault
- final var restResponse = new Rest()//NOPMD
+ final var restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/sys/seal-status")
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
- .connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
- .sslVerification(config.getSslConfig().isVerify())
- .sslContext(config.getSslConfig().getSslContext())
.get();
// Validate restResponse
diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java
index f18f5317..98011247 100644
--- a/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java
+++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Sys.java
@@ -21,9 +21,7 @@ public class Sys extends OperationsBase {
public Sys(final VaultConfig config) {
super(config);
- if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
- this.nameSpace = this.config.getNameSpace();
- }
+ this.nameSpace = this.config.getNameSpace();
}
public Sys withNameSpace(final String nameSpace) {
diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java
index 41bd2d02..b8823ff9 100644
--- a/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java
+++ b/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java
@@ -9,7 +9,6 @@
import io.github.jopenlibs.vault.response.LogicalResponse;
import io.github.jopenlibs.vault.response.UnwrapResponse;
import io.github.jopenlibs.vault.response.WrapResponse;
-import io.github.jopenlibs.vault.rest.Rest;
import io.github.jopenlibs.vault.rest.RestResponse;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
@@ -30,9 +29,7 @@ public class Wrapping extends OperationsBase {
public Wrapping(final VaultConfig config) {
super(config);
- if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
- this.nameSpace = this.config.getNameSpace();
- }
+ this.nameSpace = this.config.getNameSpace();
}
/**
@@ -43,8 +40,8 @@ public Wrapping(final VaultConfig config) {
* {@code
* final String wrappingToken = "...";
* final VaultConfig config = new VaultConfig().address(...).token(wrappingToken).build();
- * final Vault vault = new Vault(config);
- * final LogicalResponse response = vault.sys().wrapping().lookupWarp();
+ * final Vault vault = Vault.create(config);
+ * final LogicalResponse response = vault.sys().wrapping().lookupWrap();
* // Then you can validate "path" for example ...
* final String path = response.getData().get("path");
* }
@@ -64,10 +61,10 @@ public LogicalResponse lookupWrap() throws VaultException {
* ** @@ -349,7 +340,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { * final String authToken = "..."; * final String wrappingToken = "..."; * final VaultConfig config = new VaultConfig().address(...).token(authToken).build(); - * final Vault vault = new Vault(config); + * final Vault vault = Vault.create(config); * * final WrapResponse wrapResponse = vault.sys().wrapping().wrap( * // Data to wrap @@ -362,7 +353,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { * ); * * final UnwrapResponse unwrapResponse = vault.sys().wrapping().unwrap(wrapResponse.getToken(), true); - * final JsonObject unwrappedData = response.getData(); // original data + * final JsonObject unwrappedData = unwrapResponse.getData(); // original data * } * * @@ -384,14 +375,11 @@ public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws V final String url = config.getAddress() + "/v1/sys/wrapping/unwrap"; // HTTP request to Vault - var rest = new Rest() + var rest = getRest() .url(url) .header("X-Vault-Namespace", this.nameSpace) .header("X-Vault-Request", "true") - .connectTimeoutSeconds(config.getOpenTimeout()) - .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()); + .readTimeoutSeconds(config.getReadTimeout()); if (inBody) { final String requestJson = Json.object().add("token", new String(wrappedToken)).toString(); @@ -438,9 +426,9 @@ public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws V * final String authToken = "..."; * final String wrappingToken = "..."; * final VaultConfig config = new VaultConfig().address(...).token(authToken).build(); - * final Vault vault = new Vault(config); + * final Vault vault = Vault.create(config); * - * final WrapResponse wrapResponse = vault.auth().wrap( + * final WrapResponse wrapResponse = vault.sys().wrapping().wrap( * // Data to wrap * new JsonObject() * .add("foo", "bar") @@ -450,10 +438,10 @@ public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws V * 60 * ); * ... - * final WrapResponse wrapResponse2 = vault.auth().rewrap(wrapResponse.getToken()); + * final WrapResponse wrapResponse2 = vault.sys().wrapping().rewrap(wrapResponse.getToken()); * - * final UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse2.getToken()); - * final JsonObject unwrappedData = response.getData(); // original data + * final UnwrapResponse unwrapResponse = vault.sys().wrapping().unwrap(wrapResponse2.getToken()); + * final JsonObject unwrappedData = unwrapResponse.getData(); // original data * } * * @@ -471,16 +459,13 @@ public WrapResponse rewrap(final char[] wrappedToken) throws VaultException { final String url = config.getAddress() + "/v1/sys/wrapping/rewrap"; // HTTP request to Vault - final var restResponse = new Rest() + final var restResponse = getRest() .url(url) .token(config.getToken()) .header("X-Vault-Namespace", this.nameSpace) .header("X-Vault-Request", "true") .body(requestJson.getBytes(StandardCharsets.UTF_8)) - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .post(); // Validate restResponse diff --git a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java index 6d97fbf3..42b655bd 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java +++ b/src/main/java/io/github/jopenlibs/vault/api/sys/mounts/Mounts.java @@ -4,8 +4,6 @@ import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.OperationsBase; import io.github.jopenlibs.vault.response.MountResponse; -import io.github.jopenlibs.vault.rest.Rest; -import io.github.jopenlibs.vault.rest.RestResponse; import java.nio.charset.StandardCharsets; /** @@ -45,14 +43,11 @@ public Mounts(final VaultConfig config) { */ public MountResponse list() throws VaultException { return retry(attempt -> { - final var restResponse = new Rest()//NOPMD + final var restResponse = getRest()//NOPMD .url(String.format("%s/v1/sys/mounts", config.getAddress())) .token(config.getToken()) .header("X-Vault-Request", "true") - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .get(); // Validate restResponse @@ -90,7 +85,7 @@ public MountResponse list() throws VaultException { * .maxLeaseTtl(TimeToLive.of(86400, TimeUnit.SECONDS)) * .description("description for pki engine"); * - * final MountResponse response = vault.mounts().enable("pki/mount/point/path", MountType.PKI, payload); + * final MountResponse response = vault.sys().mounts().enable("pki/mount/point/path", MountType.PKI, payload); * * assertEquals(204, response.getRestResponse().getStatus(); * } @@ -115,15 +110,12 @@ public MountResponse enable(final String path, final MountType type, final Mount return retry(attempt -> { final String requestJson = payload.toEnableJson(type).toString(); - final var restResponse = new Rest()//NOPMD + final var restResponse = getRest()//NOPMD .url(String.format("%s/v1/sys/mounts/%s", config.getAddress(), path)) .token(config.getToken()) .header("X-Vault-Request", "true") .body(requestJson.getBytes(StandardCharsets.UTF_8)) - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .post(); // Validate restResponse @@ -153,7 +145,7 @@ public MountResponse enable(final String path, final MountType type, final Mount * final VaultConfig config = new VaultConfig.address(...).token(...).build(); * final Vault vault = Vault.create(config); * - * final MountResponse response = vault.mounts().disable("pki/mount/point/path"); + * final MountResponse response = vault.sys().mounts().disable("pki/mount/point/path"); * * assertEquals(204, response.getRestResponse().getStatus(); * } @@ -165,14 +157,11 @@ public MountResponse enable(final String path, final MountType type, final Mount */ public MountResponse disable(final String path) throws VaultException { return retry(attempt -> { - final var restResponse = new Rest()//NOPMD + final var restResponse = getRest()//NOPMD .url(String.format("%s/v1/sys/mounts/%s", config.getAddress(), path)) .token(config.getToken()) .header("X-Vault-Request", "true") - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .delete(); // Validate restResponse @@ -202,7 +191,7 @@ public MountResponse disable(final String path) throws VaultException { * final VaultConfig config = new VaultConfig.address(...).token(...).build(); * final Vault vault = Vault.create(config); * - * final MountResponse response = vault.mounts().read("pki/mount/point/path"); + * final MountResponse response = vault.sys().mounts().read("pki/mount/point/path"); * final Mount mount = response.getMount(); * final MountConfig mountConfig = mount.getConfig(); * } @@ -214,14 +203,11 @@ public MountResponse disable(final String path) throws VaultException { */ public MountResponse read(final String path) throws VaultException { return retry(attempt -> { - final var restResponse = new Rest()//NOPMD + final var restResponse = getRest()//NOPMD .url(String.format("%s/v1/sys/mounts/%s/tune", config.getAddress(), path)) .token(config.getToken()) .header("X-Vault-Request", "true") - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .get(); // Validate restResponse @@ -259,7 +245,7 @@ public MountResponse read(final String path) throws VaultException { * .maxLeaseTtl(TimeToLive.of(12, TimeUnit.HOURS)) * .description("description of pki"); * - * final MountResponse response = vault.mounts().tune("pki/mount/point/path", configs); + * final MountResponse response = vault.sys().mounts().tune("pki/mount/point/path", configs); * * assertEquals(204, response.getRestResponse().getStatus(); * } @@ -278,15 +264,12 @@ public MountResponse tune(final String path, final MountPayload payload) throws final String requestJson = payload.toTuneJson().toString(); - final var restResponse = new Rest()//NOPMD + final var restResponse = getRest()//NOPMD .url(String.format("%s/v1/sys/mounts/%s/tune", config.getAddress(), path)) .token(config.getToken()) .header("X-Vault-Request", "true") .body(requestJson.getBytes(StandardCharsets.UTF_8)) - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .post(); // Validate restResponse diff --git a/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java b/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java index c1c69486..354dc377 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java @@ -40,15 +40,14 @@ public AuthResponse(final RestResponse restResponse, final int retries) { final var responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); jsonResponse = Json.parse(responseJson).asObject(); final var authJsonVal = jsonResponse.get("auth"); - final var authJsonObject = - authJsonVal != null && !authJsonVal.isNull() ? authJsonVal.asObject() : null; + final var authJsonObject = hasValue(authJsonVal) ? authJsonVal.asObject() : null; if (authJsonObject != null) { authLeaseDuration = authJsonObject.getInt("lease_duration", 0); authRenewable = authJsonObject.getBoolean("renewable", false); - if (authJsonObject.get("metadata") != null && !authJsonObject.get("metadata") - .toString().equalsIgnoreCase("null")) { - final var metadata = authJsonObject.get("metadata").asObject(); + final var metadataJsonVal = authJsonObject.get("metadata"); + if (hasValue(metadataJsonVal)) { + final var metadata = metadataJsonVal.asObject(); appId = metadata.getString("app-id", ""); userId = metadata.getString("user-id", ""); username = metadata.getString("username", ""); @@ -58,13 +57,18 @@ public AuthResponse(final RestResponse restResponse, final int retries) { authClientToken = authJsonObject.getString("client_token", ""); tokenAccessor = authJsonObject.getString("accessor", ""); - final var authPoliciesJsonArray = authJsonObject.get("policies").asArray(); - authPolicies = authPoliciesJsonArray.values().stream() - .map(JsonValue::asString) - .collect(Collectors.toUnmodifiableList()); + final var authPoliciesJsonVal = authJsonObject.get("policies"); + if (hasValue(authPoliciesJsonVal)) { + authPolicies = authPoliciesJsonVal.asArray().values().stream() + .map(JsonValue::asString) + .collect(Collectors.toUnmodifiableList()); + } } - renewable = jsonResponse.get("renewable").asBoolean(); + final var renewableJsonVal = jsonResponse.get("renewable"); + if (hasValue(renewableJsonVal)) { + renewable = renewableJsonVal.asBoolean(); + } } catch (ParseException e) { // No-op. } diff --git a/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java b/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java index 5c86a7d7..8006d210 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/LookupResponse.java @@ -41,7 +41,11 @@ public LookupResponse(final RestResponse restResponse, final int retries) { try { final var responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); final var jsonObject = Json.parse(responseJson).asObject(); - final var dataJsonObject = jsonObject.get("data").asObject(); + final var dataJsonVal = jsonObject.get("data"); + if (!hasValue(dataJsonVal)) { + return; + } + final var dataJsonObject = dataJsonVal.asObject(); accessor = dataJsonObject.getString("accessor", ""); creationTime = dataJsonObject.getLong("creation_time", 0); @@ -50,21 +54,23 @@ public LookupResponse(final RestResponse restResponse, final int retries) { explicitMaxTTL = dataJsonObject.getLong("explicit_max_ttl", 0); id = dataJsonObject.getString("id", ""); final var lastRenewalTimeJsonValue = dataJsonObject.get("last_renewal_time"); - if (lastRenewalTimeJsonValue != null) { + if (hasValue(lastRenewalTimeJsonValue)) { lastRenewalTime = lastRenewalTimeJsonValue.asLong(); } - if (dataJsonObject.get("metadata") != null && !dataJsonObject.get("metadata").toString() - .equalsIgnoreCase("null")) { - final var metadata = dataJsonObject.get("metadata").asObject(); + final var metadataJsonVal = dataJsonObject.get("metadata"); + if (hasValue(metadataJsonVal)) { + final var metadata = metadataJsonVal.asObject(); username = metadata.getString("username", ""); } numUses = dataJsonObject.getInt("num_uses", 0); orphan = dataJsonObject.getBoolean("orphan", true); path = dataJsonObject.getString("path", ""); - final var policiesJsonArray = dataJsonObject.get("policies").asArray(); - policies = policiesJsonArray.values().stream() - .map(JsonValue::asString) - .collect(Collectors.toUnmodifiableList()); + final var policiesJsonVal = dataJsonObject.get("policies"); + if (hasValue(policiesJsonVal)) { + policies = policiesJsonVal.asArray().values().stream() + .map(JsonValue::asString) + .collect(Collectors.toUnmodifiableList()); + } renewable = dataJsonObject.getBoolean("renewable", false); ttl = dataJsonObject.getLong("ttl", 0); diff --git a/src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java b/src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java index daa21fef..9cc69f00 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java @@ -18,9 +18,15 @@ public UnwrapResponse(final RestResponse restResponse, final int retries) { super(restResponse, retries); } + /** + * @return The{@code * final VaultConfig config = new VaultConfig().address(...).token(authToken).build(); - * final Vault vault = new Vault(config); + * final Vault vault = Vault.create(config); * ... * final String wrappingToken = "..."; - * final LogicalResponse response = vault.sys().wrapping().lookupWarp(wrappingToken); + * final LogicalResponse response = vault.sys().wrapping().lookupWrap(wrappingToken); * // Then you can validate "path" for example ... * final String path = response.getData().get("path"); * }@@ -88,10 +85,10 @@ public LogicalResponse lookupWrap(final char[] wrappedToken) throws VaultExcepti *** @@ -212,17 +206,14 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault final String url = config.getAddress() + "/v1/sys/wrapping/wrap"; // HTTP request to Vault - final var restResponse = new Rest() + final var restResponse = getRest() .url(url) .token(config.getToken()) .header("X-Vault-Wrap-TTL", Integer.toString(ttlInSec)) .header("X-Vault-Namespace", this.nameSpace) .header("X-Vault-Request", "true") .body(requestJson.getBytes(StandardCharsets.UTF_8)) - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .post(); // Validate restResponse @@ -258,7 +249,7 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault *{@code * final VaultConfig config = new VaultConfig().address(...).token(authToken).build(); - * final Vault vault = new Vault(config); + * final Vault vault = Vault.create(config); * ... * final String wrappingToken = "..."; - * final LogicalResponse response = vault.sys().wrapping().lookupWarp(wrappingToken); + * final LogicalResponse response = vault.sys().wrapping().lookupWrap(wrappingToken); * // Then you can validate "path" for example ... * final String path = response.getData().get("path"); * }@@ -111,14 +108,11 @@ public LogicalResponse lookupWrap(final char[] wrappedToken, boolean inBody) return retry(attempt -> { // HTTP request to Vault - var rest = new Rest()//NOPMD + var rest = getRest()//NOPMD .url(config.getAddress() + "/v1/sys/wrapping/lookup") .header("X-Vault-Namespace", this.nameSpace) .header("X-Vault-Request", "true") - .connectTimeoutSeconds(config.getOpenTimeout()) - .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()); + .readTimeoutSeconds(config.getReadTimeout()); if (inBody) { rest = rest @@ -180,7 +174,7 @@ public LogicalResponse lookupWrap(final char[] wrappedToken, boolean inBody) * final String authToken = "..."; * final String wrappingToken = "..."; * final VaultConfig config = new VaultConfig().address(...).token(authToken).build(); - * final Vault vault = new Vault(config); + * final Vault vault = Vault.create(config); * * final WrapResponse wrapResponse = vault.sys().wrapping().wrap( * // Data to wrap @@ -193,7 +187,7 @@ public LogicalResponse lookupWrap(final char[] wrappedToken, boolean inBody) * ); * * final UnwrapResponse unwrapResponse = vault.sys().wrapping().unwrap(wrapResponse.getToken()); - * final JsonObject unwrappedData = response.getData(); // original data + * final JsonObject unwrappedData = unwrapResponse.getData(); // original data * } *{@code * final String wrappingToken = "..."; * final VaultConfig config = new VaultConfig().address(...).token(wrappingToken).build(); - * final Vault vault = new Vault(config); + * final Vault vault = Vault.create(config); * final AuthResponse response = vault.sys().wrapping().unwrap(); * final String unwrappedToken = response.getAuthClientToken(); * }@@ -296,7 +287,7 @@ public UnwrapResponse unwrap() throws VaultException { * final String authToken = "..."; * final String wrappingToken = "..."; * final VaultConfig config = new VaultConfig().address(...).token(authToken).build(); - * final Vault vault = new Vault(config); + * final Vault vault = Vault.create(config); * * final WrapResponse wrapResponse = vault.sys().wrapping().wrap( * // Data to wrap @@ -309,7 +300,7 @@ public UnwrapResponse unwrap() throws VaultException { * ); * * final UnwrapResponse unwrapResponse = vault.sys().wrapping().unwrap(wrapResponse.getToken()); - * final JsonObject unwrappedData = response.getData(); // original data + * final JsonObject unwrappedData = unwrapResponse.getData(); // original data * } *
data object of the unwrapped response, or null if it
+ * has none
+ */
public JsonObject getData() {
- assert jsonResponse.get("data").isObject();
-
- return jsonResponse.get("data").asObject();
+ if (jsonResponse == null) {
+ return null;
+ }
+ final var data = jsonResponse.get("data");
+ return data != null && data.isObject() ? data.asObject() : null;
}
}
diff --git a/src/main/java/io/github/jopenlibs/vault/response/VaultResponse.java b/src/main/java/io/github/jopenlibs/vault/response/VaultResponse.java
index c8524c47..bfcbc16e 100644
--- a/src/main/java/io/github/jopenlibs/vault/response/VaultResponse.java
+++ b/src/main/java/io/github/jopenlibs/vault/response/VaultResponse.java
@@ -1,5 +1,6 @@
package io.github.jopenlibs.vault.response;
+import io.github.jopenlibs.vault.json.JsonValue;
import io.github.jopenlibs.vault.rest.RestResponse;
import java.io.Serializable;
@@ -30,4 +31,11 @@ public RestResponse getRestResponse() {
public int getRetries() {
return retries;
}
+
+ /**
+ * @return true if a JSON field is present and not null
+ */
+ static boolean hasValue(final JsonValue value) {
+ return value != null && !value.isNull();
+ }
}
diff --git a/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java b/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java
index 053832c2..dcf5a06b 100644
--- a/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java
+++ b/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java
@@ -31,16 +31,20 @@ public WrapResponse(final RestResponse restResponse, final int retries) {
final var responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8);
final var jsonResponse = Json.parse(responseJson).asObject();
final var wrapInfoJsonVal = jsonResponse.get("wrap_info");
- if (wrapInfoJsonVal != null && !wrapInfoJsonVal.isNull()) {
+ if (hasValue(wrapInfoJsonVal)) {
final var wrapInfoJsonObject = wrapInfoJsonVal.asObject();
- token = wrapInfoJsonObject.getString("token", null).toCharArray();
+ final var tokenString = wrapInfoJsonObject.getString("token", null);
+ token = tokenString == null ? null : tokenString.toCharArray();
accessor = wrapInfoJsonObject.getString("accessor", null);
ttl = wrapInfoJsonObject.getInt("ttl", 0);
creationTime = wrapInfoJsonObject.getString("creation_time", null);
creationPath = wrapInfoJsonObject.getString("creation_path", null);
}
- renewable = jsonResponse.get("renewable").asBoolean();
+ final var renewableJsonVal = jsonResponse.get("renewable");
+ if (hasValue(renewableJsonVal)) {
+ renewable = renewableJsonVal.asBoolean();
+ }
} catch (ParseException e) {
// No-op.
}
diff --git a/src/main/java/io/github/jopenlibs/vault/rest/Rest.java b/src/main/java/io/github/jopenlibs/vault/rest/Rest.java
index 5cd2afcf..77c37d16 100644
--- a/src/main/java/io/github/jopenlibs/vault/rest/Rest.java
+++ b/src/main/java/io/github/jopenlibs/vault/rest/Rest.java
@@ -203,8 +203,8 @@ public Rest parameter(final String name, final String value) throws RestExceptio
* This method may be chained together repeatedly
* * @param value The vault token value as a {@code char[]} - * @return This object, with a vault token added, ready for other builder-pattern config methods or - * an HTTP verb method + * @return This object, with a vault token added, ready for other builder-pattern config methods + * or an HTTP verb method */ public Rest token(final char[] value) { if (value != null && value.length > 0) { @@ -235,7 +235,7 @@ public Rest header(final String name, final String value) { *The number of seconds to wait before giving up on establishing an HTTP(S) connection.
* * @param connectTimeoutSeconds Number of seconds to wait for an HTTP(S) connection to - * successfully establish + * successfully establish, or0 for no timeout
* @return This object, with connectTimeoutSeconds populated, ready for other builder-pattern
* config methods or an HTTP verb method
*/
@@ -249,7 +249,7 @@ public Rest connectTimeoutSeconds(final Integer connectTimeoutSeconds) {
* to wait for all data to finish downloading.
*
* @param readTimeoutSeconds Number of seconds to wait for all data to be retrieved from an
- * established HTTP(S) connection
+ * established HTTP(S) connection, or 0 for no timeout
* @return This object, with readTimeoutSeconds populated, ready for other builder-pattern
* config methods or an HTTP verb method
*/
@@ -445,27 +445,43 @@ private String parametersToQueryString() {
*/
private RestResponse send(final HttpRequest req) throws IOException, InterruptedException {
final var client = getClient();
- final var response = client.send(req, BodyHandlers.ofString());
+ final var response = client.send(req, BodyHandlers.ofByteArray());
// Get the resulting status code
final var statusCode = response.statusCode();
- // Download and parse response
+ // Keep the raw response bytes, decoding is left to the caller
final var mimeType = response.headers().firstValue("Content-Type").orElse("");
- final var body = response.body().getBytes(StandardCharsets.UTF_8);
- return new RestResponse(statusCode, mimeType, body);
+ return new RestResponse(statusCode, mimeType, response.body());
}
private HttpClient getClient() {
if (configuredClient != null) {
return configuredClient;
}
+ return newHttpClient(connectTimeoutSeconds, sslVerification, sslContext);
+ }
+
+ /**
+ * Builds an {@link HttpClient} with the given connection settings. The returned client is meant
+ * to be built once and shared between requests (see {@link #Rest(HttpClient)}), so that
+ * connections and worker threads are reused.
+ *
+ * @param connectTimeoutSeconds Number of seconds to wait for a connection, or
+ * null for no timeout
+ * @param sslVerification false to disable SSL certificate verification
+ * @param sslContext The SSLContext to use, or null for the JVM default
+ * @return A new {@link HttpClient} instance
+ */
+ public static HttpClient newHttpClient(final Integer connectTimeoutSeconds,
+ final Boolean sslVerification, final SSLContext sslContext) {
final var client = HttpClient.newBuilder()
//Stick to HTTP/1.1 by default, coz Vault Agent fails proxying h2c request to https
.version(Version.HTTP_1_1);
- if (connectTimeoutSeconds != null) {
+ // As with HttpURLConnection, 0 means no timeout
+ if (connectTimeoutSeconds != null && connectTimeoutSeconds > 0) {
client.connectTimeout(Duration.ofSeconds(connectTimeoutSeconds));
}
@@ -494,18 +510,18 @@ private Builder buildRequest(final boolean isGetOrDelete)
throw new RestException("No URL is set");
}
- final var baseUri = new URI(urlString);
+ var uri = new URI(urlString);
final var params = isGetOrDelete ? parametersToQueryString() : "";
- var query = params;
- if (baseUri.getQuery() != null) {
- query = baseUri.getQuery();
- if (!params.isEmpty()) {
- query = baseUri.getQuery() + "&" + params;
- }
+ if (!params.isEmpty()) {
+ // Work on the raw (still encoded) components: parameters are already url-encoded, and
+ // the multi-argument URI constructors would encode them again (e.g. '%' to "%25")
+ final var query = uri.getRawQuery() == null ? params : uri.getRawQuery() + "&" + params;
+ uri = new URI(uri.getScheme() + "://" + uri.getRawAuthority()
+ + (uri.getRawPath() == null ? "" : uri.getRawPath())
+ + "?" + query
+ + (uri.getRawFragment() == null ? "" : "#" + uri.getRawFragment()));
}
- final var uri = new URI(baseUri.getScheme(), baseUri.getUserInfo(), baseUri.getHost(),
- baseUri.getPort(), baseUri.getPath(), query, baseUri.getFragment());
// Initialize HTTP(S) connection, and set any header values
final var request = HttpRequest.newBuilder()
@@ -513,7 +529,8 @@ private Builder buildRequest(final boolean isGetOrDelete)
headers.forEach(request::header);
- if (readTimeoutSeconds != null) {
+ // As with HttpURLConnection, 0 means no timeout
+ if (readTimeoutSeconds != null && readTimeoutSeconds > 0) {
request.timeout(Duration.ofSeconds(readTimeoutSeconds));
}
diff --git a/src/test/java/io/github/jopenlibs/vault/ConnectionReUsageTest.java b/src/test/java/io/github/jopenlibs/vault/ConnectionReUsageTest.java
index 777b9ae2..c36dc9ab 100644
--- a/src/test/java/io/github/jopenlibs/vault/ConnectionReUsageTest.java
+++ b/src/test/java/io/github/jopenlibs/vault/ConnectionReUsageTest.java
@@ -1,8 +1,8 @@
package io.github.jopenlibs.vault;
import io.github.jopenlibs.vault.api.Logical;
-import io.github.jopenlibs.vault.response.LogicalResponse;
import io.github.jopenlibs.vault.mock.MockVault;
+import io.github.jopenlibs.vault.response.LogicalResponse;
import java.net.Socket;
import java.net.http.HttpClient;
import java.nio.ByteBuffer;
@@ -72,6 +72,29 @@ public void readShouldReuseConnectionAfterSuccessfulRequestByHttp() throws Excep
assertTrue("Too many connections opened: " + opened, opened <= (closed + 1));
}
+ @Test
+ public void readShouldReuseConnectionWithDefaultHttpClient() throws Exception {
+ int readNum = 10;
+
+ Logical vault = Vault.create(new VaultConfig()
+ .address("http://localhost:8999")
+ .token(TOKEN)
+ .openTimeout(10)
+ .readTimeout(10)
+ .engineVersion(1)
+ .build()).logical();
+
+ for (int i = 0; i < readNum; i++) {
+ LogicalResponse resp = vault.read("testing/p1");
+ assertEquals("value", resp.getData().get("key"));
+ }
+
+ int closed = connectionListener.getClosed();
+ int opened = connectionListener.getOpened();
+
+ assertTrue("Too many connections opened: " + opened, opened <= (closed + 1));
+ }
+
private static class NetworkConnectionListener implements NetworkTrafficListener {
private final AtomicInteger opened = new AtomicInteger();
@@ -109,7 +132,8 @@ public void reset() {
}
}
- public Server initHttpMockVaultWithListener(final MockVault mock, NetworkTrafficListener listener) {
+ public Server initHttpMockVaultWithListener(final MockVault mock,
+ NetworkTrafficListener listener) {
final var server = new Server();
final var http = new HttpConfiguration();
diff --git a/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java b/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java
new file mode 100644
index 00000000..659ba70e
--- /dev/null
+++ b/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java
@@ -0,0 +1,89 @@
+package io.github.jopenlibs.vault;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Unit tests for the default EnvironmentLoader, with a fake process environment and
+ * home directory.
+ */
+public class EnvironmentLoaderTests {
+
+ @Rule
+ public TemporaryFolder home = new TemporaryFolder();
+
+ private final Mapverify
+ * set to false without it being requested: verification must stay enabled.
+ */
+ @Test
+ public void testUnbuiltVerifyFalseIsNotRestored() throws Exception {
+ final var config = new SslConfig();
+ final Field verify = SslConfig.class.getDeclaredField("verify");
+ verify.setAccessible(true);
+ verify.setBoolean(config, false);
+
+ assertTrue(roundTrip(config).isVerify());
+ }
+
+ @Test
+ public void testVerifyDisabledByEnvironmentSurvives() throws Exception {
+ final var config = new SslConfig().environmentLoader(new VerifyFalseLoader()).build();
+
+ assertFalse(config.isVerify());
+ assertFalse(roundTrip(config).isVerify());
+ }
+
+ private static class VerifyFalseLoader extends EnvironmentLoader {
+
+ @Override
+ public String loadVariable(final String name) {
+ return "VAULT_SSL_VERIFY".equals(name) ? "false" : null;
+ }
+ }
+
+ /**
+ * Reads an SslConfig serialized by release 6.2.3.
+ */
+ private static SslConfig read623(final String name) throws Exception {
+ try (var in = new ObjectInputStream(SslConfigSerializationTests.class
+ .getResourceAsStream("/serialized-6.2.3/" + name))) {
+ return (SslConfig) in.readObject();
+ }
+ }
+
+ @Test
+ public void testRelease623UnbuiltConfigVerifies() throws Exception {
+ assertTrue(read623("sslconfig-unbuilt.ser").isVerify());
+ }
+
+ @Test
+ public void testRelease623BuiltConfigVerifies() throws Exception {
+ assertTrue(read623("sslconfig-built.ser").isVerify());
+ }
+
+ @Test
+ public void testRelease623VerifyDisabledSurvives() throws Exception {
+ assertFalse(read623("sslconfig-verify-false.ser").isVerify());
+ }
+
+ @Test
+ public void testPemCertificateSurvives() throws Exception {
+ final String pem = certPem();
+ final var copy = roundTrip(new SslConfig().pemUTF8(pem).build());
+
+ assertEquals(pem, copy.getPemUTF8());
+ assertNotNull(copy.getSslContext());
+ }
+
+ /**
+ * A deserialized, already built SslConfig is not rebuilt by
+ * VaultConfig.build().
+ */
+ @Test
+ public void testBuiltStateSurvives() throws Exception {
+ final var copy = roundTrip(new SslConfig().pemUTF8(certPem()).build());
+ final var sslContext = copy.getSslContext();
+
+ new VaultConfig().address("https://127.0.0.1:8200").sslConfig(copy).build();
+ assertSame(sslContext, copy.getSslContext());
+ }
+
+ @Test
+ public void testVaultConfigSslSettingsSurvive() throws Exception {
+ final var config = new VaultConfig()
+ .address("https://127.0.0.1:8200")
+ .sslConfig(new SslConfig().pemUTF8(certPem()))
+ .build();
+ final var copy = roundTrip(config);
+
+ assertTrue(copy.getSslConfig().isVerify());
+ assertNotNull(copy.getSslConfig().getSslContext());
+ }
+}
diff --git a/src/test/java/io/github/jopenlibs/vault/TimeoutTests.java b/src/test/java/io/github/jopenlibs/vault/TimeoutTests.java
index b0b91185..948afaf8 100644
--- a/src/test/java/io/github/jopenlibs/vault/TimeoutTests.java
+++ b/src/test/java/io/github/jopenlibs/vault/TimeoutTests.java
@@ -102,4 +102,42 @@ public void testReadTimeout_BeyondThreshold() throws Exception {
}
}
+
+ /**
+ * A timeout of 0 means no timeout, as with the former HttpURLConnection client.
+ */
+ @Test
+ public void testZeroTimeouts_MeanNoTimeout() throws Exception {
+ // Mock Vault takes 2 seconds to respond
+ final var readTimeoutsMockVault = new ReadTimeoutsMockVault(2, 200,
+ "{\"data\":{\"value\":\"mock\"}}");
+ final var server = VaultTestUtils.initHttpMockVault(readTimeoutsMockVault);
+ server.start();
+
+ final var vaultConfig = new VaultConfig()
+ .address("http://127.0.0.1:8999")
+ .token("mock_token")
+ .openTimeout(0)
+ .readTimeout(0)
+ .engineVersion(1)
+ .build();
+ final var vault = Vault.create(vaultConfig);
+
+ try {
+ final var response = vault.logical().read("secret/hello");
+ org.junit.Assert.assertEquals("mock", response.getData().get("value"));
+ } finally {
+ VaultTestUtils.shutdownMockVault(server);
+ }
+ }
+
+ @Test(expected = VaultException.class)
+ public void testNegativeOpenTimeout_IsRejected() throws VaultException {
+ new VaultConfig().address("http://127.0.0.1:8999").openTimeout(-1).build();
+ }
+
+ @Test(expected = VaultException.class)
+ public void testNegativeReadTimeout_IsRejected() throws VaultException {
+ new VaultConfig().address("http://127.0.0.1:8999").readTimeout(-1).build();
+ }
}
diff --git a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java
index afc8c32e..d79b1e3c 100644
--- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java
+++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java
@@ -1,17 +1,31 @@
package io.github.jopenlibs.vault;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.io.PrintWriter;
+import java.net.http.HttpClient;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import org.junit.After;
import org.junit.Assert;
+import org.junit.Before;
import org.junit.Test;
import static junit.framework.TestCase.assertEquals;
@@ -254,4 +268,397 @@ public void testConfigBuilder_WithNamespace() throws VaultException {
.build();
Assert.assertEquals(vaultConfig.getNameSpace(), "namespace");
}
+
+ private final Listfalse may disable SSL verification.
+ */
+ @Test
+ public void testConfigBuilder_LoadFromEnv_SslVerifyFalse() throws VaultException {
+ for (final String value : new String[]{"false", "FALSE", " false\n"}) {
+ Assert.assertFalse("\"" + value + "\"", sslVerifyFromEnv(value));
+ }
+ Assert.assertEquals(List.of(), sslConfigWarnings);
+ }
+
+ /**
+ * The recognized true values, unset and empty values enable SSL verification without warnings.
+ */
+ @Test
+ public void testConfigBuilder_LoadFromEnv_SslVerifyTrue() throws VaultException {
+ for (final String value : new String[]{"t", "T", "true", "TRUE", " true", "1", "y", "Y", "yes", "YES", "on", "On\n", ""}) {
+ Assert.assertTrue("\"" + value + "\"", sslVerifyFromEnv(value));
+ }
+ Assert.assertTrue(sslVerifyFromEnv(null));
+ Assert.assertEquals(List.of(), sslConfigWarnings);
+ }
+
+ /**
+ * Any other value, including typos and other "false-like" values, keeps SSL verification
+ * enabled, and logs a warning.
+ */
+ @Test
+ public void testConfigBuilder_LoadFromEnv_SslVerifyFailsClosed() throws VaultException {
+ final String[] values = {"0", "f", "n", "no", "off", "ture", "disabled"};
+ for (final String value : values) {
+ Assert.assertTrue("\"" + value + "\"", sslVerifyFromEnv(value));
+ }
+ Assert.assertEquals(values.length, sslConfigWarnings.size());
+ }
+
+ /**
+ * An explicit verify() setting takes priority over the environment variable.
+ */
+ @Test
+ public void testConfigBuilder_SslVerifyExplicitOverridesEnv() throws VaultException {
+ final var mock = new MockEnvironmentLoader();
+ mock.override("VAULT_SSL_VERIFY", "false");
+ Assert.assertTrue(new SslConfig().environmentLoader(mock).verify(true).build().isVerify());
+
+ mock.override("VAULT_SSL_VERIFY", "true");
+ Assert.assertFalse(new SslConfig().environmentLoader(mock).verify(false).build().isVerify());
+ }
+
+ /**
+ * Empty or blank values from any environment loader, even a custom one, are treated as unset.
+ */
+ @Test
+ public void testConfigBuilder_LoadFromEnv_EmptyAddressIsUnset() {
+ final var mock = new MockEnvironmentLoader();
+ mock.override("VAULT_ADDR", "");
+ try {
+ new VaultConfig().environmentLoader(mock).build();
+ Assert.fail("Expected VaultException");
+ } catch (VaultException e) {
+ assertEquals("No address is set", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testConfigBuilder_LoadFromEnv_EmptyValuesAreUnset() throws VaultException {
+ final var mock = new MockEnvironmentLoader();
+ mock.override("VAULT_ADDR", "https://127.0.0.1:8200");
+ mock.override("VAULT_TOKEN", "");
+ mock.override("VAULT_OPEN_TIMEOUT", "");
+ mock.override("VAULT_READ_TIMEOUT", " ");
+ mock.override("VAULT_SSL_CERT", "");
+
+ final var config = new VaultConfig().environmentLoader(mock).build();
+ Assert.assertNull(config.getToken());
+ Assert.assertNull(config.getOpenTimeout());
+ Assert.assertNull(config.getReadTimeout());
+ Assert.assertNull(config.getSslConfig().getSslContext());
+ }
+
+ /**
+ * Surrounding whitespace (e.g. a trailing newline from a mounted secret) is removed from
+ * values of any environment loader, even a custom one.
+ */
+ @Test
+ public void testConfigBuilder_LoadFromEnv_ValuesAreTrimmed() throws IOException, VaultException {
+ final var pemFile = Files.createTempFile("cert", ".pem");
+ try (InputStream input = this.getClass().getResourceAsStream("/cert.pem")) {
+ Files.write(pemFile, input.readAllBytes());
+ }
+ try {
+ final var mock = new MockEnvironmentLoader();
+ mock.override("VAULT_ADDR", " https://127.0.0.1:8200\n");
+ mock.override("VAULT_TOKEN", "s.token\n");
+ mock.override("VAULT_OPEN_TIMEOUT", " 5");
+ mock.override("VAULT_READ_TIMEOUT", "30\n");
+ mock.override("VAULT_SSL_CERT", pemFile + "\n");
+
+ final var config = new VaultConfig().environmentLoader(mock).build();
+ assertEquals("https://127.0.0.1:8200", config.getAddress());
+ assertEquals("s.token", new String(config.getToken()));
+ assertEquals(Integer.valueOf(5), config.getOpenTimeout());
+ assertEquals(Integer.valueOf(30), config.getReadTimeout());
+ Assert.assertNotNull(config.getSslConfig().getSslContext());
+ } finally {
+ Files.delete(pemFile);
+ }
+ }
+
+ /**
+ * A zero timeout from the environment is kept (it means no timeout), a negative one is
+ * ignored.
+ */
+ @Test
+ public void testConfigBuilder_LoadFromEnv_ZeroAndNegativeTimeouts() throws VaultException {
+ final var mock = new MockEnvironmentLoader();
+ mock.override("VAULT_ADDR", "http://127.0.0.1:8200");
+ mock.override("VAULT_OPEN_TIMEOUT", "0");
+ mock.override("VAULT_READ_TIMEOUT", "-1");
+
+ final var config = new VaultConfig().environmentLoader(mock).build();
+ assertEquals(Integer.valueOf(0), config.getOpenTimeout());
+ Assert.assertNull(config.getReadTimeout());
+ }
+
+ /**
+ * VAULT_TOKEN is not looked up when a token was set explicitly, since the default
+ * loader may read the ~/.vault-token file for it.
+ */
+ @Test
+ public void testConfigBuilder_ExplicitTokenSkipsEnvLookup() throws VaultException {
+ final ListSslConfig that was never built must not disable SSL verification.
+ */
+ @Test
+ public void testUnbuiltSslConfigVerifies() {
+ Assert.assertTrue(new SslConfig().isVerify());
+ }
+
+ /**
+ * An explicit verify() value is honored whether or not build() was
+ * called, and also when set after it.
+ */
+ @Test
+ public void testSslVerifyExplicitValueAlwaysWins() throws VaultException {
+ Assert.assertFalse(new SslConfig().verify(false).isVerify());
+ Assert.assertFalse(new SslConfig().build().verify(false).isVerify());
+
+ final var mock = new MockEnvironmentLoader();
+ mock.override("VAULT_SSL_VERIFY", "false");
+ Assert.assertTrue(new SslConfig().environmentLoader(mock).build().verify(true).isVerify());
+ }
+
+ /**
+ * VaultConfig.build() builds an SslConfig whose own
+ * build() was not called, so a supplied certificate is actually used.
+ */
+ @Test
+ public void testBuildBuildsPassedSslConfig() throws VaultException {
+ final var sslConfig = new SslConfig().pemResource("/cert.pem");
+ Assert.assertNull(sslConfig.getSslContext());
+
+ final var config = new VaultConfig()
+ .environmentLoader(new MockEnvironmentLoader())
+ .address("https://127.0.0.1:8200")
+ .sslConfig(sslConfig)
+ .build();
+ Assert.assertTrue(config.getSslConfig().isVerify());
+ Assert.assertNotNull(sslConfig.getSslContext());
+ }
+
+ /**
+ * A passed SslConfig without its own environment loader uses the one of the
+ * VaultConfig.
+ */
+ @Test
+ public void testBuildPassesEnvironmentLoaderToSslConfig() throws VaultException {
+ final var mock = new MockEnvironmentLoader();
+ mock.override("VAULT_SSL_VERIFY", "false");
+ final var config = new VaultConfig()
+ .environmentLoader(mock)
+ .address("https://127.0.0.1:8200")
+ .sslConfig(new SslConfig())
+ .build();
+ Assert.assertFalse(config.getSslConfig().isVerify());
+ }
+
+ /**
+ * An already built SslConfig is left untouched.
+ */
+ @Test
+ public void testBuildDoesNotRebuildBuiltSslConfig() throws VaultException {
+ final var sslEnv = new MockEnvironmentLoader();
+ sslEnv.override("VAULT_SSL_VERIFY", "false");
+ final var sslConfig = new SslConfig().environmentLoader(sslEnv).build();
+
+ final var vaultEnv = new MockEnvironmentLoader();
+ vaultEnv.override("VAULT_SSL_VERIFY", "true");
+ final var config = new VaultConfig()
+ .environmentLoader(vaultEnv)
+ .address("https://127.0.0.1:8200")
+ .sslConfig(sslConfig)
+ .build();
+ Assert.assertFalse(config.getSslConfig().isVerify());
+
+ final var pemConfig = new SslConfig().pemResource("/cert.pem").build();
+ final var sslContext = pemConfig.getSslContext();
+ new VaultConfig()
+ .environmentLoader(new MockEnvironmentLoader())
+ .address("https://127.0.0.1:8200")
+ .sslConfig(pemConfig)
+ .build();
+ Assert.assertSame(sslContext, pemConfig.getSslContext());
+ }
+
+ /**
+ * An invalid certificate in a passed SslConfig is reported by
+ * VaultConfig.build().
+ */
+ @Test(expected = VaultException.class)
+ public void testBuildFailsOnInvalidPassedSslConfig() throws VaultException {
+ new VaultConfig()
+ .environmentLoader(new MockEnvironmentLoader())
+ .address("https://127.0.0.1:8200")
+ .sslConfig(new SslConfig().pemUTF8("not a certificate"))
+ .build();
+ }
+
+ /**
+ * An environment loader with no variables set, that can be serialized (unlike the inner
+ * MockEnvironmentLoader, which holds a reference to the test instance).
+ */
+ static class EmptyEnvironmentLoader extends EnvironmentLoader {
+
+ @Override
+ public String loadVariable(final String name) {
+ return null;
+ }
+ }
+
+ /**
+ * A VaultConfig with a preconfigured HttpClient (which is not
+ * serializable) can be serialized. The deserialized copy falls back to its own default client.
+ */
+ @Test
+ public void testSerializationWithCustomHttpClient() throws Exception {
+ final HttpClient httpClient = HttpClient.newHttpClient();
+ final VaultConfig config = new VaultConfig()
+ .environmentLoader(new EmptyEnvironmentLoader())
+ .address("http://127.0.0.1:8200")
+ .token("token")
+ .httpClient(httpClient)
+ .build();
+
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
+ out.writeObject(config);
+ }
+ final VaultConfig copy;
+ try (ObjectInputStream in = new ObjectInputStream(
+ new ByteArrayInputStream(bytes.toByteArray()))) {
+ copy = (VaultConfig) in.readObject();
+ }
+
+ assertEquals("http://127.0.0.1:8200", copy.getAddress());
+ Assert.assertNotNull(copy.getHttpClient());
+ Assert.assertNotSame(httpClient, copy.getHttpClient());
+ Assert.assertSame(copy.getHttpClient(), copy.getHttpClient());
+ }
+
+ /**
+ * The default HttpClient is reused while its settings are unchanged, and rebuilt when the
+ * passed SslConfig is changed after VaultConfig.build().
+ */
+ @Test
+ public void testDefaultHttpClientFollowsSslConfigChanges() throws Exception {
+ final String pem;
+ try (InputStream input = this.getClass().getResourceAsStream("/cert.pem")) {
+ pem = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ final SslConfig sslConfig = new SslConfig().verify(false);
+ final VaultConfig config = new VaultConfig()
+ .environmentLoader(new MockEnvironmentLoader())
+ .address("https://127.0.0.1:8200")
+ .sslConfig(sslConfig)
+ .build();
+
+ final HttpClient unverified = config.getHttpClient();
+ Assert.assertSame(unverified, config.getHttpClient());
+
+ // Verification turned on, with new certificates: build() creates a new SSLContext
+ sslConfig.verify(true).pemUTF8(pem).build();
+ final HttpClient withPem = config.getHttpClient();
+ Assert.assertNotSame(unverified, withPem);
+ Assert.assertSame(sslConfig.getSslContext(), withPem.sslContext());
+ Assert.assertSame(withPem, config.getHttpClient());
+
+ // Verification turned off, without calling build() again
+ sslConfig.verify(false);
+ final HttpClient unverifiedAgain = config.getHttpClient();
+ Assert.assertNotSame(withPem, unverifiedAgain);
+ Assert.assertNotSame(sslConfig.getSslContext(), unverifiedAgain.sslContext());
+
+ // A new connect timeout
+ config.openTimeout(5);
+ final HttpClient withTimeout = config.getHttpClient();
+ Assert.assertNotSame(unverifiedAgain, withTimeout);
+ assertEquals(Duration.ofSeconds(5), withTimeout.connectTimeout().orElse(null));
+ }
+
+ /**
+ * A null or blank address is treated as unset, so VAULT_ADDR is used.
+ */
+ @Test
+ public void testConfigBuilder_NullOrBlankAddressFallsBackToEnv() throws VaultException {
+ final MockEnvironmentLoader mock = new MockEnvironmentLoader();
+ mock.override("VAULT_ADDR", "http://127.0.0.1:8200");
+
+ assertEquals("http://127.0.0.1:8200",
+ new VaultConfig().environmentLoader(mock).address(null).build().getAddress());
+ assertEquals("http://127.0.0.1:8200",
+ new VaultConfig().environmentLoader(mock).address(" \n").build().getAddress());
+ }
+
+ /**
+ * A prefix path depth lower than 1 is rejected with a message stating the valid range.
+ */
+ @Test
+ public void testPrefixPathDepthBelowOneIsRejected() {
+ try {
+ new VaultConfig().prefixPath("/");
+ Assert.fail("Expected an IllegalArgumentException to be thrown");
+ } catch (IllegalArgumentException e) {
+ assertEquals("prefixPathDepth must be at least 1, was: 0", e.getMessage());
+ }
+ }
}
diff --git a/src/test/java/io/github/jopenlibs/vault/VaultTests.java b/src/test/java/io/github/jopenlibs/vault/VaultTests.java
index 16694db4..eec1ca93 100644
--- a/src/test/java/io/github/jopenlibs/vault/VaultTests.java
+++ b/src/test/java/io/github/jopenlibs/vault/VaultTests.java
@@ -11,6 +11,28 @@
*/
public class VaultTests {
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullEngineVersionIsRejected() throws VaultException {
+ final var config = new VaultConfig().address("http://127.0.0.1:8200").build();
+ Vault.create(config, (Integer) null);
+ }
+
+ @Test
+ public void testNullUseSecretsEnginePathMapIsFalse() throws VaultException {
+ final var config = new VaultConfig().address("http://127.0.0.1:8200").build();
+ final var vault = Vault.create(config, null, 1);
+ Assert.assertEquals(Integer.valueOf(1),
+ vault.logical().getEngineVersionForSecretPath("secret/hello"));
+ }
+
+ @Test
+ public void testUnsetEngineVersionDefaultsTo2() throws VaultException {
+ final var config = new VaultConfig().address("http://127.0.0.1:8200").build();
+ final var vault = Vault.create(config, false, null);
+ Assert.assertEquals(Integer.valueOf(2),
+ vault.logical().getEngineVersionForSecretPath("secret/hello"));
+ }
+
@Test
public void testDefaultVaultConstructor() {
var vaultConfig = new VaultConfig();
@@ -137,4 +159,28 @@ public void testConfigBuiler_WithInvalidRequestAsNonError() throws Exception {
Assert.assertEquals(403, response.getRestResponse().getStatus());
Assert.assertEquals(0, response.getRetries());
}
+
+ /**
+ * When the KV Engine version map cannot be generated (here, Vault denies access to
+ * sys/mounts), a VaultException with a clear message is thrown.
+ */
+ @Test
+ public void testVaultWithKVEnginePathMapAccessDenied() throws Exception {
+ final var server = VaultTestUtils.initHttpMockVault(new MockVault(403, "{}"));
+ server.start();
+ try {
+ final var vaultConfig = new VaultConfig()
+ .address("http://127.0.0.1:8999")
+ .token("mock_token")
+ .build();
+ Vault.create(vaultConfig, true, 1);
+ Assert.fail("Expected a VaultException to be thrown");
+ } catch (VaultException e) {
+ Assert.assertEquals(
+ "An Engine KV version map was not supplied, and unable to determine KV "
+ + "Engine version. Do you have admin rights?", e.getMessage());
+ } finally {
+ VaultTestUtils.shutdownMockVault(server);
+ }
+ }
}
diff --git a/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAppIdIT.java b/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAppIdIT.java
index e88a39bd..092112ef 100644
--- a/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAppIdIT.java
+++ b/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAppIdIT.java
@@ -17,7 +17,10 @@
* Integration tests for the AppId auth backend. According to the Vault documentation, this method
* is deprecated. Also, from Vault 1.11.7 it fails the usage. For this reason, we skip this test if
* Vault version is greater than 1.11.6
+ *
+ * @deprecated CI only tests Vault 1.16 and later, so this test is always skipped there
*/
+@Deprecated
public class AuthBackendAppIdIT {
@ClassRule
diff --git a/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAwsTests.java b/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAwsTests.java
index 9fd1af42..eb0277e3 100644
--- a/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAwsTests.java
+++ b/src/test/java/io/github/jopenlibs/vault/api/AuthBackendAwsTests.java
@@ -3,10 +3,10 @@
import io.github.jopenlibs.vault.Vault;
import io.github.jopenlibs.vault.VaultConfig;
import io.github.jopenlibs.vault.VaultException;
-import io.github.jopenlibs.vault.json.JsonObject;
-import io.github.jopenlibs.vault.response.AuthResponse;
import io.github.jopenlibs.vault.VaultTestUtils;
+import io.github.jopenlibs.vault.json.JsonObject;
import io.github.jopenlibs.vault.mock.AuthRequestValidatingMockVault;
+import io.github.jopenlibs.vault.response.AuthResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.util.function.Predicate;
import org.junit.Test;
diff --git a/src/test/java/io/github/jopenlibs/vault/api/AuthBackendPkiIT.java b/src/test/java/io/github/jopenlibs/vault/api/AuthBackendPkiIT.java
index a25bad60..4353e3b5 100644
--- a/src/test/java/io/github/jopenlibs/vault/api/AuthBackendPkiIT.java
+++ b/src/test/java/io/github/jopenlibs/vault/api/AuthBackendPkiIT.java
@@ -98,6 +98,7 @@ void issueCredentialTemplate(FunctionThis class is used to mock out a Vault server in unit tests verifying that response bodies
+ * are passed through byte for byte. It responds to every request with HTTP 200 and the
+ * pre-determined bytes, sent as application/octet-stream.
null JSON fields.
+ */
+public class ResponseNullHandlingTests {
+
+ private static RestResponse json(final String body) {
+ return new RestResponse(200, "application/json", body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void testAuthResponseWithoutPoliciesOrRenewable() {
+ final var response = new AuthResponse(
+ json("{\"auth\":{\"client_token\":\"token\",\"metadata\":null}}"), 0);
+ assertEquals("token", response.getAuthClientToken());
+ assertNull(response.getAuthPolicies());
+ assertNull(response.getRenewable());
+ }
+
+ @Test
+ public void testAuthResponseWithNullFields() {
+ final var response = new AuthResponse(
+ json("{\"auth\":{\"policies\":null},\"renewable\":null}"), 0);
+ assertNull(response.getAuthPolicies());
+ assertNull(response.getRenewable());
+ }
+
+ @Test
+ public void testWrapResponseWithoutToken() {
+ final var response = new WrapResponse(json("{\"wrap_info\":{\"accessor\":\"acc\"}}"), 0);
+ assertNull(response.getToken());
+ assertEquals("acc", response.getAccessor());
+ assertNull(response.getRenewable());
+ }
+
+ @Test
+ public void testLookupResponseWithNullFields() {
+ final var response = new LookupResponse(
+ json("{\"data\":{\"id\":\"id\",\"last_renewal_time\":null,\"metadata\":null}}"), 0);
+ assertEquals("id", response.getId());
+ assertNull(response.getLastRenewalTime());
+ assertNull(response.getPolicies());
+ }
+
+ @Test
+ public void testLookupResponseWithoutData() {
+ final var response = new LookupResponse(json("{\"errors\":[]}"), 0);
+ assertNull(response.getId());
+ }
+
+ @Test
+ public void testUnwrapResponseWithoutData() {
+ final var response = new UnwrapResponse(json("{\"auth\":{\"client_token\":\"t\"}}"), 0);
+ assertNull(response.getData());
+ }
+}
diff --git a/src/test/java/io/github/jopenlibs/vault/rest/DeleteTests.java b/src/test/java/io/github/jopenlibs/vault/rest/DeleteTests.java
index c9fe2287..a2a81993 100644
--- a/src/test/java/io/github/jopenlibs/vault/rest/DeleteTests.java
+++ b/src/test/java/io/github/jopenlibs/vault/rest/DeleteTests.java
@@ -1,7 +1,7 @@
package io.github.jopenlibs.vault.rest;
-import io.github.jopenlibs.vault.json.Json;
import io.github.jopenlibs.vault.VaultTestUtils;
+import io.github.jopenlibs.vault.json.Json;
import io.github.jopenlibs.vault.mock.EchoInputMockVault;
import org.junit.Test;
diff --git a/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java b/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java
index 84934ce9..1f1ee75a 100644
--- a/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java
+++ b/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java
@@ -1,7 +1,7 @@
package io.github.jopenlibs.vault.rest;
-import io.github.jopenlibs.vault.json.Json;
import io.github.jopenlibs.vault.VaultTestUtils;
+import io.github.jopenlibs.vault.json.Json;
import io.github.jopenlibs.vault.mock.EchoInputMockVault;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.server.Server;
@@ -119,6 +119,27 @@ public void testGet_UpdateParams() throws RestException {
args.getString("multi part", null));
}
+ /**
+ * Parameters containing reserved characters (and '%') are url-encoded exactly once, so the
+ * server receives their original values.
+ */
+ @Test
+ public void testGet_ParamsWithReservedCharacters() throws RestException {
+ final var restResponse = new Rest()
+ .url(this.URL + "?hot=cold")
+ .parameter("key", "a/b c&d=e%f?g#h")
+ .get();
+ assertEquals(200, restResponse.getStatus());
+
+ final var jsonString = new String(restResponse.getBody(), StandardCharsets.UTF_8);
+ final var jsonObject = Json.parse(jsonString).asObject();
+ assertEquals("http://127.0.0.1:8999/?hot=cold&key=a%2Fb+c%26d%3De%25f%3Fg%23h",
+ jsonObject.getString("URL", null));
+ final var args = jsonObject.get("args").asObject();
+ assertEquals("cold", args.getString("hot", null));
+ assertEquals("a/b c&d=e%f?g#h", args.getString("key", null));
+ }
+
/**
* Verify a GET request that passes HTTP headers.
* diff --git a/src/test/java/io/github/jopenlibs/vault/rest/PostTests.java b/src/test/java/io/github/jopenlibs/vault/rest/PostTests.java index 68533f3c..7363fd33 100644 --- a/src/test/java/io/github/jopenlibs/vault/rest/PostTests.java +++ b/src/test/java/io/github/jopenlibs/vault/rest/PostTests.java @@ -1,7 +1,7 @@ package io.github.jopenlibs.vault.rest; -import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.VaultTestUtils; +import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.mock.EchoInputMockVault; import java.nio.charset.StandardCharsets; import org.eclipse.jetty.server.Server; diff --git a/src/test/java/io/github/jopenlibs/vault/rest/PutTests.java b/src/test/java/io/github/jopenlibs/vault/rest/PutTests.java index 9115b729..15aabbd3 100644 --- a/src/test/java/io/github/jopenlibs/vault/rest/PutTests.java +++ b/src/test/java/io/github/jopenlibs/vault/rest/PutTests.java @@ -1,7 +1,7 @@ package io.github.jopenlibs.vault.rest; -import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.VaultTestUtils; +import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.mock.EchoInputMockVault; import java.nio.charset.StandardCharsets; import org.eclipse.jetty.server.Server; diff --git a/src/test/java/io/github/jopenlibs/vault/rest/ResponseBodyTests.java b/src/test/java/io/github/jopenlibs/vault/rest/ResponseBodyTests.java new file mode 100644 index 00000000..a8bc43b0 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/rest/ResponseBodyTests.java @@ -0,0 +1,51 @@ +package io.github.jopenlibs.vault.rest; + +import io.github.jopenlibs.vault.VaultTestUtils; +import io.github.jopenlibs.vault.mock.BinaryBodyMockVault; +import org.eclipse.jetty.server.Server; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * Unit tests verifying that the REST client returns response bodies unaltered. + */ +public class ResponseBodyTests { + + private static final byte[] BINARY_BODY = new byte[256]; + + static { + for (int i = 0; i < BINARY_BODY.length; i++) { + BINARY_BODY[i] = (byte) i; + } + } + + private Server server; + + @Before + public void startServer() throws Exception { + this.server = VaultTestUtils.initHttpMockVault(new BinaryBodyMockVault(BINARY_BODY)); + this.server.start(); + } + + @After + public void stopServer() throws Exception { + VaultTestUtils.shutdownMockVault(this.server); + } + + /** + * A body that is not valid UTF-8 (e.g. a DER certificate) must be returned byte for byte. + */ + @Test + public void testBinaryBodyIsNotAltered() throws RestException { + final var restResponse = new Rest() + .url("http://127.0.0.1:8999/") + .get(); + assertEquals(200, restResponse.getStatus()); + assertEquals("application/octet-stream", restResponse.getMimeType()); + assertArrayEquals(BINARY_BODY, restResponse.getBody()); + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/util/SSLUtils.java b/src/test/java/io/github/jopenlibs/vault/util/SSLUtils.java index ad79c9c7..e2c6d3ae 100644 --- a/src/test/java/io/github/jopenlibs/vault/util/SSLUtils.java +++ b/src/test/java/io/github/jopenlibs/vault/util/SSLUtils.java @@ -6,6 +6,7 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -20,7 +21,6 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Date; import java.util.Map; diff --git a/src/test/java/io/github/jopenlibs/vault/util/VaultContainer.java b/src/test/java/io/github/jopenlibs/vault/util/VaultContainer.java index 5f1690d7..526e7070 100644 --- a/src/test/java/io/github/jopenlibs/vault/util/VaultContainer.java +++ b/src/test/java/io/github/jopenlibs/vault/util/VaultContainer.java @@ -118,7 +118,11 @@ public void initAndUnsealVault() throws IOException, InterruptedException { /** * Prepares the Vault server for testing of the AppID auth backend (i.e. mounts the backend and * populates test data). + * + * @deprecated The AppID auth backend is not available in Vault 1.16 and later, the versions + * tested in CI */ + @Deprecated public void setupBackendAppId() throws IOException, InterruptedException { runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); diff --git a/src/test/resources/serialized-6.2.3/sslconfig-built.ser b/src/test/resources/serialized-6.2.3/sslconfig-built.ser new file mode 100644 index 00000000..4b00553e Binary files /dev/null and b/src/test/resources/serialized-6.2.3/sslconfig-built.ser differ diff --git a/src/test/resources/serialized-6.2.3/sslconfig-unbuilt.ser b/src/test/resources/serialized-6.2.3/sslconfig-unbuilt.ser new file mode 100644 index 00000000..8342c789 Binary files /dev/null and b/src/test/resources/serialized-6.2.3/sslconfig-unbuilt.ser differ diff --git a/src/test/resources/serialized-6.2.3/sslconfig-verify-false.ser b/src/test/resources/serialized-6.2.3/sslconfig-verify-false.ser new file mode 100644 index 00000000..9147badc Binary files /dev/null and b/src/test/resources/serialized-6.2.3/sslconfig-verify-false.ser differ