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 @@ org.slf4j slf4j-simple - 2.0.5 + 2.0.7 test @@ -120,15 +120,42 @@ + org.apache.maven.plugins maven-compiler-plugin + 3.13.0 11 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + ${project.basedir}/config/checkstyle/checkstyle.xml + true + true + true + warning + + + + checkstyle + validate + + check + + + + + + org.apache.maven.plugins maven-source-plugin + 3.4.0 attach-sources @@ -140,7 +167,9 @@ + org.apache.maven.plugins maven-javadoc-plugin + 3.12.0 attach-javadocs @@ -161,7 +190,6 @@ maven-surefire-plugin 3.2.5 - ${project.basedir}/test **/*Test.java **/*Tests.java @@ -227,7 +255,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.6.3 + 3.12.0 attach-javadocs diff --git a/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java b/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java index 0e2cbfc7..78614fe4 100644 --- a/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java +++ b/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.Serializable; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; /** @@ -11,30 +12,62 @@ */ public class EnvironmentLoader implements Serializable { + // The value computed for release 6.2.3, which declared none: keeps its serialized configs readable + private static final long serialVersionUID = 1109558394682383170L; + + /** + * Loads an environment variable. Surrounding whitespace (e.g. a trailing newline) is removed, + * and an empty or blank value is treated as unset. + * + *

If VAULT_TOKEN is unset, it is read from the .vault-token file + * in the executing user's home directory, if any.

+ * + * @param name The environment variable name + * @return The variable value, or 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 List VERIFY_TRUE_VALUES = List.of("t", "true", "1", "y", "yes", "on"); + private static final Logger LOGGER = Logger.getLogger(SslConfig.class.getCanonicalName()); - private boolean verify; + private boolean verify = true; private transient SSLContext sslContext; private transient KeyStore trustStore; private transient KeyStore keyStore; @@ -57,6 +62,7 @@ public class SslConfig implements Serializable { private String clientKeyPemUTF8; private Boolean verifyObject; private EnvironmentLoader environmentLoader; + private boolean built; /** *

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 SslConfig 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.

* * @param verify Whether or not to verify the SSL certificate used by Vault with HTTPS * connections. Default is 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.

+ * + * @return 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(); Map getSecretEngineVersions(); diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index a7327d3f..c480c487 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -1,10 +1,14 @@ package io.github.jopenlibs.vault; +import io.github.jopenlibs.vault.rest.Rest; import java.io.Serializable; import java.net.http.HttpClient; import java.util.Collections; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; +import javax.net.ssl.SSLContext; /** *

A container for the configuration settings needed to initialize 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 Map secretsEnginePathMap = new ConcurrentHashMap<>(); private String address; private char[] token; @@ -45,7 +51,9 @@ public class VaultConfig implements Serializable { private Integer globalEngineVersion; private String nameSpace; private EnvironmentLoader environmentLoader; - private HttpClient httpClient; + // HttpClient is not serializable: a deserialized config falls back to the default client + private transient HttpClient httpClient; + private transient volatile DefaultHttpClient defaultHttpClient; /** *

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 * VaultConfig object will fail.

* + *

A null, empty or blank address is treated as unset.

+ * * @param address The Vault server base URL * @return This object, with address populated, ready for additional builder-pattern method * calls or else finalization with the build() method */ public VaultConfig address(final String address) { - this.address = address.trim(); + if (address == null || address.isBlank()) { + this.address = null; + return this; + } + this.address = address.strip(); if (this.address.endsWith("/")) { this.address = this.address.substring(0, this.address.length() - 1); } @@ -188,6 +202,10 @@ public VaultConfig putSecretsEngineVersionForPath(String path, String version) { * 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.

+ * * @param sslConfig SSL-related configuration options * @return This object, with SSL configuration options populated, ready for additional * builder-pattern method calls or else finalization with the build() method @@ -206,7 +224,7 @@ public VaultConfig sslConfig(final SslConfig sslConfig) { * environment variable.

* * @param openTimeout Number of seconds to wait for an HTTP(S) connection to successfully - * establish + * establish, or 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.

* * @param httpClient preconfigured http client instance * @return VaultConfig @@ -344,7 +366,8 @@ void setEngineVersion(final Integer engineVersion) { * * @return This object, with all available config options parsed and loaded * @throws VaultException If the 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 Map getSecretsEnginePathMap() { return Collections.unmodifiableMap(secretsEnginePathMap); } @@ -437,7 +480,62 @@ public int getPrefixPathDepth() { return prefixPathDepth; } + /** + *

Returns 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 secretsEnginePathMap; try { - logger.info( - "No secrets Engine version map was supplied, attempting to generate one."); - final Map secretsEnginePathMap = collectSecretEngineVersions(); - assert secretsEnginePathMap != null; - this.vaultConfig.secretsEnginePathMap(secretsEnginePathMap); - } catch (Exception e) { + secretsEnginePathMap = collectSecretEngineVersions(); + } catch (RuntimeException e) { throw new VaultException(String.format( - "An Engine KV version map was not supplied, and unable to determine " + - "KV Engine " + - "version, " + "due to exception: %s", - e.getMessage() + ". Do you have admin rights?")); + "An Engine KV version map was not supplied, and unable to determine KV " + + "Engine version, due to exception: %s. Do you have admin rights?", + e.getMessage())); } + // null if Vault could not be reached or did not answer with HTTP 200 + if (secretsEnginePathMap == null) { + throw new VaultException( + "An Engine KV version map was not supplied, and unable to determine KV " + + "Engine version. Do you have admin rights?"); + } + this.vaultConfig.secretsEnginePathMap(secretsEnginePathMap); + } + } + + private void logNameSpace() { + final String nameSpace = this.vaultConfig.getNameSpace(); + if (nameSpace != null && !nameSpace.isEmpty()) { + logger.info(String.format( + "The Namespace %s has been bound to this Vault instance. Please keep this in mind when running operations.", + nameSpace)); } } @@ -249,8 +249,11 @@ public Database database(final String mountPath) { * Returns the implementing class for Vault's lease operations (e.g. revoke, revoke-prefix). * * @return The implementing class for Vault's lease operations (e.g. revoke, revoke-prefix). + * + * @deprecated This method is deprecated and in future it will be removed */ @Override + @Deprecated public Leases leases() { return new Leases(vaultConfig); } @@ -270,8 +273,11 @@ public Debug debug() { * /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 Map collectSecretEngineVersions() { try { - final var restResponse = new Rest()//NOPMD + final var restResponse = new Rest(vaultConfig.getHttpClient())//NOPMD .url(vaultConfig.getAddress() + "/v1/sys/mounts") .token(vaultConfig.getToken()) .header("X-Vault-Namespace", this.vaultConfig.getNameSpace()) .header("X-Vault-Request", "true") - .connectTimeoutSeconds(vaultConfig.getOpenTimeout()) .readTimeoutSeconds(vaultConfig.getReadTimeout()) - .sslVerification(vaultConfig.getSslConfig().isVerify()) - .sslContext(vaultConfig.getSslConfig().getSslContext()) .get(); if (restResponse.getStatus() != 200) { return null; @@ -339,7 +345,7 @@ private Map collectSecretEngineVersions() { } return data; } catch (RestException e) { - System.err.print( + logger.warning( String.format("Unable to retrieve the KV Engine secrets, due to exception: %s", e.getMessage())); return null; diff --git a/src/main/java/io/github/jopenlibs/vault/api/Auth.java b/src/main/java/io/github/jopenlibs/vault/api/Auth.java index d84fcc1a..42017d04 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Auth.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Auth.java @@ -256,9 +256,7 @@ public String getEntityAlias() { public Auth(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 Auth withNameSpace(final String nameSpace) { @@ -380,10 +378,7 @@ public AuthResponse createToken(final TokenRequest tokenRequest, final String to .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 @@ -414,6 +409,8 @@ public AuthResponse createToken(final TokenRequest tokenRequest, final String to * @param userId The user-id used for authentication * @return The auth token, with additional response metadata * @throws VaultException If any error occurs, or unexpected response received from Vault + * @deprecated The App ID auth backend is no longer available in the Vault versions this driver + * is tested against (1.16 and later). Use {@link #loginByAppRole(String, String)} instead. */ @Deprecated public AuthResponse loginByAppID(final String path, final String appId, final String userId) @@ -426,10 +423,7 @@ public AuthResponse loginByAppID(final String path, final String appId, final St .url(config.getAddress() + "/v1/auth/" + path) .header("X-Vault-Namespace", this.nameSpace) .body(requestJson.getBytes(StandardCharsets.UTF_8)) - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .post(); // Validate restResponse @@ -502,10 +496,7 @@ public AuthResponse loginByAppRole(final String path, final String roleId, .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 @@ -567,10 +558,7 @@ public AuthResponse loginByUserPass(final String username, final String password .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 @@ -674,10 +662,7 @@ public AuthResponse loginByAwsEc2(final String role, final String identity, .body(requestJson.getBytes(StandardCharsets.UTF_8)) .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 restResponse @@ -730,10 +715,7 @@ public AuthResponse loginByAwsEc2(final String role, final String pkcs7, final S .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 @@ -793,10 +775,7 @@ public AuthResponse loginByAwsIam(final String role, final String iamRequestUrl, .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 @@ -854,10 +833,7 @@ public AuthResponse loginByGithub(final String githubToken, final String githubA .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 @@ -923,10 +899,7 @@ public AuthResponse loginByJwt(final String provider, final String role, final S .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 @@ -1069,10 +1042,7 @@ public AuthResponse loginByCert(final String certAuthMount) throws VaultExceptio .url(config.getAddress() + "/v1/auth/" + mount + "/login") .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 restResponse @@ -1130,10 +1100,7 @@ public AuthResponse renewSelf(final long increment, final String tokenAuthMount) .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 restResponse @@ -1171,10 +1138,7 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept .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 restResponse @@ -1187,6 +1151,7 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept * @see io.github.jopenlibs.vault.api.sys.Wrapping#lookupWrap() * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public LogicalResponse lookupWrap() throws VaultException { final var sys = new Sys(this.config); return sys.wrapping().lookupWrap(config.getToken(), false); @@ -1196,6 +1161,7 @@ public LogicalResponse lookupWrap() throws VaultException { * @see io.github.jopenlibs.vault.api.sys.Wrapping#lookupWrap(char[]) * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public LogicalResponse lookupWrap(final char[] wrappedToken) throws VaultException { final var sys = new Sys(this.config); return sys.wrapping().lookupWrap(wrappedToken, true); @@ -1205,6 +1171,7 @@ public LogicalResponse lookupWrap(final char[] wrappedToken) throws VaultExcepti * @see io.github.jopenlibs.vault.api.sys.Wrapping#lookupWrap(char[], boolean) * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public LogicalResponse lookupWrap(final char[] wrappedToken, boolean inBody) throws VaultException { final var sys = new Sys(this.config); @@ -1237,10 +1204,7 @@ public void revokeSelf(final String tokenAuthMount) 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()) .post(); // Validate restResponse @@ -1254,6 +1218,7 @@ public void revokeSelf(final String tokenAuthMount) throws VaultException { * @see io.github.jopenlibs.vault.api.sys.Wrapping#unwrap() * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public UnwrapResponse unwrap() throws VaultException { final var sys = new Sys(this.config); return sys.wrapping().unwrap(config.getToken(), false); @@ -1263,6 +1228,7 @@ public UnwrapResponse unwrap() throws VaultException { * @see io.github.jopenlibs.vault.api.sys.Wrapping#unwrap(char[]) * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { final var sys = new Sys(this.config); return sys.wrapping().unwrap(wrappedToken, true); @@ -1272,6 +1238,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken) throws VaultException { * @see io.github.jopenlibs.vault.api.sys.Wrapping#unwrap(char[], boolean) * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws VaultException { final var sys = new Sys(this.config); return sys.wrapping().unwrap(wrappedToken, inBody); @@ -1281,6 +1248,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws V * @see io.github.jopenlibs.vault.api.sys.Wrapping#wrap(JsonObject, int) * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws VaultException { final var sys = new Sys(this.config); return sys.wrapping().wrap(jsonObject, ttlInSec); @@ -1290,6 +1258,7 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault * @see io.github.jopenlibs.vault.api.sys.Wrapping#rewrap(char[]) * @deprecated This method is deprecated and in future it will be removed */ + @Deprecated public WrapResponse rewrap(final char[] wrappedToken) throws VaultException { final var sys = new Sys(this.config); return sys.wrapping().rewrap(wrappedToken); diff --git a/src/main/java/io/github/jopenlibs/vault/api/Debug.java b/src/main/java/io/github/jopenlibs/vault/api/Debug.java index 2422595e..52832f34 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Debug.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Debug.java @@ -23,9 +23,7 @@ public class Debug extends OperationsBase { public Debug(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 Debug withNameSpace(final String nameSpace) { @@ -109,10 +107,7 @@ public HealthResponse health( .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()); + .readTimeoutSeconds(config.getReadTimeout()); // Add params if present if (standbyOk != null) { rest.parameter("standbyok", standbyOk.toString()); diff --git a/src/main/java/io/github/jopenlibs/vault/api/Logical.java b/src/main/java/io/github/jopenlibs/vault/api/Logical.java index 70507ee3..ca7c91e7 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Logical.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Logical.java @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; +import java.util.Objects; import static io.github.jopenlibs.vault.api.LogicalUtilities.adjustPathForDelete; import static io.github.jopenlibs.vault.api.LogicalUtilities.adjustPathForList; @@ -38,9 +39,7 @@ public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, list public Logical(final VaultConfig config) { super(config); - if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) { - this.nameSpace = this.config.getNameSpace(); - } + this.nameSpace = this.config.getNameSpace(); } /** @@ -92,10 +91,7 @@ private LogicalResponse read(final String path, final logicalOperations operatio .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 - don't treat 4xx class errors as exceptions (except 412), we want to return an error as the response @@ -152,10 +148,7 @@ public LogicalResponse read(final String path, Boolean shouldRetry, final Intege .header("X-Vault-Namespace", this.nameSpace) .header("X-Vault-Request", "true") .parameter("version", version.toString()) - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .get(); // Validate response - don't treat 4xx class errors as exceptions (except 412), we want to return an error as the response @@ -292,10 +285,7 @@ private LogicalResponse write(final String path, final Map nameV .header("X-Vault-Namespace", this.nameSpace) .header("X-Vault-Request", "true") .header("X-Vault-Wrap-TTL", wrapTTL != null ? wrapTTL.toString() : null) - .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) - .sslVerification(config.getSslConfig().isVerify()) - .sslContext(config.getSslConfig().getSslContext()) .post(); // HTTP Status should be either 200 (with content - e.g. PKI write) or 204 (no content) @@ -401,10 +391,7 @@ private LogicalResponse delete(final String path, final Logical.logicalOperation .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 @@ -451,10 +438,7 @@ public LogicalResponse delete(final String path, final int[] versions) throws Va .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()) .body(versionsToDelete.toString().getBytes(StandardCharsets.UTF_8)) .post(); @@ -511,10 +495,7 @@ public LogicalResponse unDelete(final String path, final int[] versions) throws .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()) .body(versionsToUnDelete.toString().getBytes(StandardCharsets.UTF_8)) .post(); @@ -558,10 +539,7 @@ public LogicalResponse destroy(final String path, final int[] versions) throws V .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()) .body(versionsToDestroy.toString().getBytes(StandardCharsets.UTF_8)) .post(); @@ -595,10 +573,7 @@ public LogicalResponse upgrade(final String kvPath) 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()) .body(kvToUpgrade.toString().getBytes(StandardCharsets.UTF_8)) .post(); @@ -633,7 +608,8 @@ public LogicalResponse upgrade(final String kvPath) throws VaultException { * * * @param secretPath The Vault secret path to check (e.g. secret/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 T retry(final EndpointOperation op) throws VaultException { } /** - * + * Runs an operation, retrying it up to 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 T retry(final EndpointOperation op, int retryCount, long retryIntervalMs) throws VaultException { @@ -32,10 +36,8 @@ static T retry(final EndpointOperation op, int retryCount, long retryInte return op.run(attempt); } catch (final Exception e) { // If there are retries to perform, then pause for the configured interval and then execute the loop again... - if (attempt < retryCount) { + if (attempt < retryCount && isRetryable(e) && sleep(retryIntervalMs)) { attempt++; - - sleep(retryIntervalMs); } else if (e instanceof VaultException) { // ... otherwise, give up. throw (VaultException) e; @@ -62,11 +64,33 @@ public interface EndpointOperation { T run(int attempt) throws Exception; } - private static void sleep(long delay) { + /** + * Tells whether a failed operation may succeed if run again: I/O errors (e.g. connection + * refused, timeout), and HTTP 5xx, 408, 412 and 429 responses. Other 4xx responses, invalid + * arguments and unexpected payloads fail the same way on every attempt. + */ + static boolean isRetryable(final Exception e) { + if (e instanceof VaultException) { + final int status = ((VaultException) e).getHttpStatusCode(); + return status >= 500 || status == 408 || status == 412 || status == 429; + } + if (e instanceof RestException) { + return e.getCause() instanceof IOException; + } + return e instanceof IOException; + } + + /** + * @return 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 { *
*
{@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 *
*
{@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 * } *
* @@ -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 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 * } *
* @@ -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 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, or 0 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 Map env = new HashMap<>(); + + private final EnvironmentLoader loader = new EnvironmentLoader() { + @Override + String getenv(final String name) { + return env.get(name); + } + + @Override + Path userHome() { + return home.getRoot().toPath(); + } + }; + + private void writeTokenFile(final String content) throws IOException { + Files.writeString(home.getRoot().toPath().resolve(".vault-token"), content); + } + + @Test + public void testValueIsReturned() { + env.put("VAULT_ADDR", "http://127.0.0.1:8200"); + assertEquals("http://127.0.0.1:8200", loader.loadVariable("VAULT_ADDR")); + } + + @Test + public void testValueIsTrimmed() { + env.put("VAULT_ADDR", " http://127.0.0.1:8200\n"); + assertEquals("http://127.0.0.1:8200", loader.loadVariable("VAULT_ADDR")); + env.put("VAULT_TOKEN", "from-env\r\n"); + assertEquals("from-env", loader.loadVariable("VAULT_TOKEN")); + } + + @Test + public void testUnsetEmptyAndBlankValuesAreNull() { + assertNull(loader.loadVariable("VAULT_ADDR")); + env.put("VAULT_ADDR", ""); + assertNull(loader.loadVariable("VAULT_ADDR")); + env.put("VAULT_ADDR", " \n"); + assertNull(loader.loadVariable("VAULT_ADDR")); + } + + @Test + public void testTokenFromEnvironmentWinsOverFile() throws IOException { + writeTokenFile("from-file"); + env.put("VAULT_TOKEN", "from-env"); + assertEquals("from-env", loader.loadVariable("VAULT_TOKEN")); + } + + @Test + public void testEmptyTokenFallsBackToFile() throws IOException { + writeTokenFile("from-file\n"); + env.put("VAULT_TOKEN", ""); + assertEquals("from-file", loader.loadVariable("VAULT_TOKEN")); + } + + @Test + public void testBlankTokenFileIsNull() throws IOException { + writeTokenFile(" \n"); + assertNull(loader.loadVariable("VAULT_TOKEN")); + } + + @Test + public void testMissingTokenFileIsNull() { + assertNull(loader.loadVariable("VAULT_TOKEN")); + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/RetryTests.java b/src/test/java/io/github/jopenlibs/vault/RetryTests.java index 4728359e..2b021f9b 100644 --- a/src/test/java/io/github/jopenlibs/vault/RetryTests.java +++ b/src/test/java/io/github/jopenlibs/vault/RetryTests.java @@ -1,7 +1,7 @@ package io.github.jopenlibs.vault; -import io.github.jopenlibs.vault.response.LogicalResponse; import io.github.jopenlibs.vault.mock.RetriesMockVault; +import io.github.jopenlibs.vault.response.LogicalResponse; import java.util.Map; import org.junit.Test; diff --git a/src/test/java/io/github/jopenlibs/vault/SSLTests.java b/src/test/java/io/github/jopenlibs/vault/SSLTests.java index 432b33d0..fd09cd4c 100644 --- a/src/test/java/io/github/jopenlibs/vault/SSLTests.java +++ b/src/test/java/io/github/jopenlibs/vault/SSLTests.java @@ -1,7 +1,7 @@ package io.github.jopenlibs.vault; -import io.github.jopenlibs.vault.response.LogicalResponse; import io.github.jopenlibs.vault.mock.MockVault; +import io.github.jopenlibs.vault.response.LogicalResponse; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -10,9 +10,9 @@ import java.security.KeyStore; import java.util.List; import java.util.Map; +import javax.net.ssl.SSLContext; import org.junit.Test; -import javax.net.ssl.SSLContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java b/src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java new file mode 100644 index 00000000..3622bc03 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java @@ -0,0 +1,138 @@ +package io.github.jopenlibs.vault; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests verifying that SSL settings survive Java serialization. + */ +public class SslConfigSerializationTests { + + @SuppressWarnings("unchecked") + private static T roundTrip(final T object) throws IOException, ClassNotFoundException { + final var bytes = new ByteArrayOutputStream(); + try (var out = new ObjectOutputStream(bytes)) { + out.writeObject(object); + } + try (var in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + return (T) in.readObject(); + } + } + + private static String certPem() throws IOException { + try (var input = SslConfigSerializationTests.class.getResourceAsStream("/cert.pem")) { + return new String(input.readAllBytes(), StandardCharsets.UTF_8); + } + } + + @Test + public void testVerifyEnabledSurvives() throws Exception { + assertTrue(roundTrip(new SslConfig().verify(true).build()).isVerify()); + } + + @Test + public void testVerifyDisabledSurvives() throws Exception { + assertFalse(roundTrip(new SslConfig().verify(false).build()).isVerify()); + } + + /** + * A stream from an older release, or from a never-built instance, carries verify + * 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 List sslConfigWarnings = new ArrayList<>(); + + private final Handler sslConfigWarningsHandler = new Handler() { + @Override + public void publish(final LogRecord record) { + if (record.getLevel() == Level.WARNING) { + sslConfigWarnings.add(record); + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + + @Before + public void captureSslConfigWarnings() { + Logger.getLogger(SslConfig.class.getCanonicalName()).addHandler(sslConfigWarningsHandler); + } + + @After + public void releaseSslConfigWarnings() { + Logger.getLogger(SslConfig.class.getCanonicalName()) + .removeHandler(sslConfigWarningsHandler); + } + + private boolean sslVerifyFromEnv(final String value) throws VaultException { + final var mock = new MockEnvironmentLoader(); + mock.override("VAULT_ADDR", "http://127.0.0.1:8200"); + mock.override("VAULT_SSL_VERIFY", value); + return new VaultConfig().environmentLoader(mock).build().getSslConfig().isVerify(); + } + + /** + * Only an explicit false 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 List loaded = new ArrayList<>(); + final MockEnvironmentLoader mock = new MockEnvironmentLoader() { + @Override + public String loadVariable(final String name) { + loaded.add(name); + return super.loadVariable(name); + } + }; + mock.override("VAULT_TOKEN", "from-env"); + + final var config = new VaultConfig() + .environmentLoader(mock) + .address("http://127.0.0.1:8200") + .token("explicit") + .build(); + assertEquals("explicit", new String(config.getToken())); + Assert.assertFalse(loaded.contains("VAULT_TOKEN")); + } + + /** + * An SslConfig 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(Function pkiResponseFunction) thr .allowSubdomains(true) .maxTtl("9h") ); + // Deprecated: Vault before 1.13 (no longer tested in CI) returns 204 int statusCode = VaultVersion.lessThan("1.13.0") ? 204 : 200; int responseCode = createRoleResponse.getRestResponse().getStatus(); TestCase.assertEquals(statusCode, responseCode); @@ -159,6 +160,7 @@ void issueCredentialWithCsrTemplate(BiFunction pkiResp .allowSubdomains(true) .maxTtl("9h") ); + // Deprecated: Vault before 1.13 (no longer tested in CI) returns 204 int statusCode = VaultVersion.lessThan("1.13.0") ? 204 : 200; int responseCode = createRoleResponse.getRestResponse().getStatus(); TestCase.assertEquals(statusCode, responseCode); @@ -177,6 +179,8 @@ public void testIssueCredentialWithCsr() throws VaultException, InterruptedException, NoSuchAlgorithmException { // Test not work in Vault versions less than 1.11.0 + // Deprecated: true for every pinned Vault version in CI (1.16 and later), but false for + // "latest", where this test is skipped assumeTrue(VaultVersion.greatThan("1.11.0")); issueCredentialWithCsrTemplate((pki, csr) -> { @@ -194,6 +198,8 @@ public void testIssueCredentialWithCsrAndPrivateKeyFormat() throws VaultException, InterruptedException, NoSuchAlgorithmException { // Test not works in Vault versions less than 1.11.0 + // Deprecated: true for every pinned Vault version in CI (1.16 and later), but false for + // "latest", where this test is skipped assumeTrue(VaultVersion.greatThan("1.11.0")); issueCredentialWithCsrTemplate((pki, csr) -> { @@ -217,6 +223,7 @@ public void testRevocation() throws VaultException, InterruptedException { .allowSubdomains(true) .maxTtl("9h") ); + // Deprecated: Vault before 1.13 (no longer tested in CI) returns 204 int statusCode = VaultVersion.lessThan("1.13.0") ? 204 : 200; int responseCode = createRoleResponse.getRestResponse().getStatus(); TestCase.assertEquals(statusCode, responseCode); diff --git a/src/test/java/io/github/jopenlibs/vault/api/AuthLookupTest.java b/src/test/java/io/github/jopenlibs/vault/api/AuthLookupTest.java index dcd9751a..f1985d79 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/AuthLookupTest.java +++ b/src/test/java/io/github/jopenlibs/vault/api/AuthLookupTest.java @@ -2,11 +2,11 @@ import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultTestUtils; import io.github.jopenlibs.vault.json.JsonArray; import io.github.jopenlibs.vault.json.JsonObject; -import io.github.jopenlibs.vault.response.LookupResponse; -import io.github.jopenlibs.vault.VaultTestUtils; import io.github.jopenlibs.vault.mock.MockVault; +import io.github.jopenlibs.vault.response.LookupResponse; import java.util.Optional; import org.eclipse.jetty.server.Server; import org.junit.After; diff --git a/src/test/java/io/github/jopenlibs/vault/api/LogicalIT.java b/src/test/java/io/github/jopenlibs/vault/api/LogicalIT.java index 7ca24b66..c558488f 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/LogicalIT.java +++ b/src/test/java/io/github/jopenlibs/vault/api/LogicalIT.java @@ -307,6 +307,8 @@ public void testList() throws VaultException { */ @Test public void testListSubKeys() throws VaultException { + // Deprecated: true for every pinned Vault version in CI (1.16 and later), but false for + // "latest", where this test is skipped assumeTrue(VaultVersion.greatThan("1.9.10")); final Vault vault = container.getRootVault(); diff --git a/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java b/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java new file mode 100644 index 00000000..e339f8a8 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java @@ -0,0 +1,118 @@ +package io.github.jopenlibs.vault.api; + +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.rest.Rest; +import io.github.jopenlibs.vault.rest.RestException; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Unit tests for the retry logic shared by all API operations. + */ +public class OperationsBaseRetryTests { + + @After + public void clearInterrupt() { + Thread.interrupted(); + } + + @Test + public void testTransientHttpStatusesAreRetryable() { + for (final int status : new int[]{408, 412, 429, 500, 502, 503}) { + assertTrue("HTTP " + status, + OperationsBase.isRetryable(new VaultException("error", status))); + } + } + + @Test + public void testPermanentHttpStatusesAreNotRetryable() { + for (final int status : new int[]{200, 204, 400, 401, 403, 404, 405}) { + assertFalse("HTTP " + status, + OperationsBase.isRetryable(new VaultException("error", status))); + } + } + + @Test + public void testIoErrorsAreRetryable() { + assertTrue(OperationsBase.isRetryable(connectionRefused())); + assertTrue(OperationsBase.isRetryable(new IOException("reset"))); + } + + @Test + public void testOtherErrorsAreNotRetryable() throws Exception { + assertFalse(OperationsBase.isRetryable(new VaultException("MountPayload is missing"))); + assertFalse(OperationsBase.isRetryable(new RuntimeException("unexpected payload"))); + assertFalse(OperationsBase.isRetryable(new IllegalArgumentException())); + try { + new Rest().get(); + fail("Expected RestException for missing URL"); + } catch (RestException e) { + assertFalse(OperationsBase.isRetryable(e)); + } + } + + @Test + public void testRetriesTransientErrorUntilSuccess() throws Exception { + final var calls = new AtomicInteger(); + final String result = OperationsBase.retry(attempt -> { + if (calls.incrementAndGet() < 3) { + throw new VaultException("unavailable", 503); + } + return "ok:" + attempt; + }, 5, 1); + + assertEquals("ok:2", result); + assertEquals(3, calls.get()); + } + + @Test + public void testDoesNotRetryPermanentError() { + final var calls = new AtomicInteger(); + final var error = new VaultException("permission denied", 403); + try { + OperationsBase.retry(attempt -> { + calls.incrementAndGet(); + throw error; + }, 5, 1); + fail("Expected VaultException"); + } catch (VaultException e) { + assertSame(error, e); + } + assertEquals(1, calls.get()); + } + + @Test + public void testStopsRetryingWhenInterrupted() { + final var calls = new AtomicInteger(); + final var error = new VaultException("unavailable", 503); + Thread.currentThread().interrupt(); + try { + OperationsBase.retry(attempt -> { + calls.incrementAndGet(); + throw error; + }, 5, 10_000); + fail("Expected VaultException"); + } catch (VaultException e) { + assertSame(error, e); + } + assertEquals(1, calls.get()); + assertTrue(Thread.currentThread().isInterrupted()); + } + + private static RestException connectionRefused() { + try { + new Rest().url("http://127.0.0.1:1/").connectTimeoutSeconds(1).get(); + } catch (RestException e) { + return e; + } + throw new AssertionError("Expected connection to port 1 to fail"); + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/api/TransitApiTest.java b/src/test/java/io/github/jopenlibs/vault/api/TransitApiTest.java index 07d5c329..9f03a675 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/TransitApiTest.java +++ b/src/test/java/io/github/jopenlibs/vault/api/TransitApiTest.java @@ -2,11 +2,11 @@ import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultTestUtils; import io.github.jopenlibs.vault.json.JsonArray; import io.github.jopenlibs.vault.json.JsonObject; -import io.github.jopenlibs.vault.response.LogicalResponse; -import io.github.jopenlibs.vault.VaultTestUtils; import io.github.jopenlibs.vault.mock.MockVault; +import io.github.jopenlibs.vault.response.LogicalResponse; import java.util.Map; import java.util.Optional; import org.eclipse.jetty.server.Server; diff --git a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingLookupWrapTest.java b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingLookupWrapTest.java index a093fb0c..e11bb33c 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingLookupWrapTest.java +++ b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingLookupWrapTest.java @@ -2,10 +2,10 @@ import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; -import io.github.jopenlibs.vault.json.JsonObject; -import io.github.jopenlibs.vault.response.LogicalResponse; import io.github.jopenlibs.vault.VaultTestUtils; +import io.github.jopenlibs.vault.json.JsonObject; import io.github.jopenlibs.vault.mock.MockVault; +import io.github.jopenlibs.vault.response.LogicalResponse; import java.util.Optional; import org.eclipse.jetty.server.Server; import org.junit.After; diff --git a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapTest.java b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapTest.java index 69cc85ce..5b4cbae2 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapTest.java +++ b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapTest.java @@ -2,11 +2,11 @@ import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultTestUtils; import io.github.jopenlibs.vault.json.JsonArray; 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.mock.MockVault; +import io.github.jopenlibs.vault.response.AuthResponse; import org.eclipse.jetty.server.Server; import org.junit.After; import org.junit.Before; diff --git a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapWithoutAuthResponseTest.java b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapWithoutAuthResponseTest.java index 15657ee7..4a7db3e8 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapWithoutAuthResponseTest.java +++ b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingUnwrapWithoutAuthResponseTest.java @@ -2,11 +2,11 @@ import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultTestUtils; import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.json.JsonObject; -import io.github.jopenlibs.vault.response.UnwrapResponse; -import io.github.jopenlibs.vault.VaultTestUtils; import io.github.jopenlibs.vault.mock.MockVault; +import io.github.jopenlibs.vault.response.UnwrapResponse; import org.eclipse.jetty.server.Server; import org.junit.After; import org.junit.Test; diff --git a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingWrapTest.java b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingWrapTest.java index 86d4e033..06477a0f 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingWrapTest.java +++ b/src/test/java/io/github/jopenlibs/vault/api/sys/WrappingWrapTest.java @@ -2,10 +2,10 @@ import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; -import io.github.jopenlibs.vault.json.JsonObject; -import io.github.jopenlibs.vault.response.WrapResponse; import io.github.jopenlibs.vault.VaultTestUtils; +import io.github.jopenlibs.vault.json.JsonObject; import io.github.jopenlibs.vault.mock.MockVault; +import io.github.jopenlibs.vault.response.WrapResponse; import org.eclipse.jetty.server.Server; import org.junit.After; import org.junit.Before; diff --git a/src/test/java/io/github/jopenlibs/vault/mock/BinaryBodyMockVault.java b/src/test/java/io/github/jopenlibs/vault/mock/BinaryBodyMockVault.java new file mode 100644 index 00000000..77d4ade6 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/mock/BinaryBodyMockVault.java @@ -0,0 +1,34 @@ +package io.github.jopenlibs.vault.mock; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import org.eclipse.jetty.server.Request; + +/** + *

This 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.

+ */ +public class BinaryBodyMockVault extends MockVault { + + private final byte[] mockBody; + + public BinaryBodyMockVault(final byte[] mockBody) { + this.mockBody = Arrays.copyOf(mockBody, mockBody.length); + } + + @Override + public void handle( + final String target, + final Request baseRequest, + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException { + response.setContentType("application/octet-stream"); + response.setStatus(200); + baseRequest.setHandled(true); + response.getOutputStream().write(mockBody); + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/response/ResponseNullHandlingTests.java b/src/test/java/io/github/jopenlibs/vault/response/ResponseNullHandlingTests.java new file mode 100644 index 00000000..48b59edd --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/response/ResponseNullHandlingTests.java @@ -0,0 +1,64 @@ +package io.github.jopenlibs.vault.response; + +import io.github.jopenlibs.vault.rest.RestResponse; +import java.nio.charset.StandardCharsets; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Unit tests verifying that response classes tolerate missing and 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