From f1bf44ca0b08996d708cd9ddd29762b960b67807 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 22:37:12 +0200 Subject: [PATCH 01/34] Reused HttpClient object --- .../github/jopenlibs/vault/VaultConfig.java | 34 +++++++++++++++++-- .../io/github/jopenlibs/vault/VaultImpl.java | 4 +-- .../jopenlibs/vault/api/sys/Leases.java | 10 +++--- .../github/jopenlibs/vault/api/sys/Seal.java | 7 ++-- .../jopenlibs/vault/api/sys/Wrapping.java | 9 +++-- .../vault/api/sys/mounts/Mounts.java | 12 +++---- .../io/github/jopenlibs/vault/rest/Rest.java | 20 +++++++++-- .../vault/ConnectionReUsageTest.java | 28 +++++++++++++-- 8 files changed, 92 insertions(+), 32 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index a7327d3f..f62d55ea 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -1,5 +1,6 @@ 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; @@ -46,6 +47,7 @@ public class VaultConfig implements Serializable { private String nameSpace; private EnvironmentLoader environmentLoader; private HttpClient httpClient; + private transient volatile HttpClient defaultHttpClient; /** *

The code used to load environment variables is encapsulated here, so that a mock version @@ -194,6 +196,7 @@ public VaultConfig putSecretsEngineVersionForPath(String path, String version) { */ public VaultConfig sslConfig(final SslConfig sslConfig) { this.sslConfig = sslConfig; + this.defaultHttpClient = null; return this; } @@ -212,6 +215,7 @@ public VaultConfig sslConfig(final SslConfig sslConfig) { */ public VaultConfig openTimeout(final Integer openTimeout) { this.openTimeout = openTimeout; + this.defaultHttpClient = null; return this; } @@ -286,8 +290,8 @@ 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. * * @param httpClient preconfigured http client instance * @return VaultConfig @@ -390,6 +394,7 @@ public VaultConfig build() throws VaultException { if (this.sslConfig == null) { this.sslConfig = new SslConfig().environmentLoader(this.environmentLoader).build(); } + this.defaultHttpClient = null; return this; } @@ -437,7 +442,30 @@ 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 built once and shared by all calls, so connections and + * worker threads are reused.

+ * + * @return The HttpClient used by REST API calls + */ public HttpClient getHttpClient() { - return httpClient; + if (httpClient != null) { + return httpClient; + } + var client = defaultHttpClient; + if (client == null) { + synchronized (this) { + client = defaultHttpClient; + if (client == null) { + client = Rest.newHttpClient(openTimeout, + sslConfig == null ? null : sslConfig.isVerify(), + sslConfig == null ? null : sslConfig.getSslContext()); + defaultHttpClient = client; + } + } + } + return client; } } diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 9b824655..eab96b9a 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; @@ -307,7 +305,7 @@ 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()) 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..8265f964 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; @@ -54,7 +52,7 @@ 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) @@ -95,7 +93,7 @@ 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) @@ -137,7 +135,7 @@ 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) @@ -185,7 +183,7 @@ 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) 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..129cce03 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; @@ -45,7 +44,7 @@ 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) @@ -86,7 +85,7 @@ 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") @@ -111,7 +110,7 @@ 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") 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..11d35920 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; @@ -111,7 +110,7 @@ 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") @@ -212,7 +211,7 @@ 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)) @@ -384,7 +383,7 @@ 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") @@ -471,7 +470,7 @@ 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) 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..423d89f7 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,7 +43,7 @@ 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") @@ -115,7 +113,7 @@ 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") @@ -165,7 +163,7 @@ 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") @@ -214,7 +212,7 @@ 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") @@ -278,7 +276,7 @@ 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") 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..0cc22a87 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) { @@ -461,6 +461,22 @@ 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); 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(); From 67566e702af4d5a02d5a72748ea0a6e84fb9c1c6 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 22:43:07 +0200 Subject: [PATCH 02/34] Returned response at same mode as returned from remote resource --- .../io/github/jopenlibs/vault/rest/Rest.java | 7 ++- .../vault/mock/BinaryBodyMockVault.java | 34 +++++++++++++ .../vault/rest/ResponseBodyTests.java | 51 +++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/test/java/io/github/jopenlibs/vault/mock/BinaryBodyMockVault.java create mode 100644 src/test/java/io/github/jopenlibs/vault/rest/ResponseBodyTests.java 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 0cc22a87..acfb1203 100644 --- a/src/main/java/io/github/jopenlibs/vault/rest/Rest.java +++ b/src/main/java/io/github/jopenlibs/vault/rest/Rest.java @@ -445,16 +445,15 @@ 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() { 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/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()); + } +} From 068dc2079e8820a48d71511a775b809506763370 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:04:07 +0200 Subject: [PATCH 03/34] Checked if an operation is retryable --- .../jopenlibs/vault/api/OperationsBase.java | 34 ++++- .../vault/api/OperationsBaseRetryTests.java | 118 ++++++++++++++++++ 2 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 src/test/java/io/github/jopenlibs/vault/api/OperationsBaseRetryTests.java 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/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"); + } +} From 99002eb5eef6a1daa4120c44efe8a68b38042054 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:08:03 +0200 Subject: [PATCH 04/34] Fixed CI --- .github/workflows/ci.yml | 4 +++- .github/workflows/test-report.yml | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc8fe83b..6a0dd527 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,4 +64,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' - From fd9514eb33c198fed93e9aa77ddd8ee45b76bbfd Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:14:28 +0200 Subject: [PATCH 05/34] Removed Travis CI --- .travis.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .travis.yml 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 From cbdb991a4f17328fdd1493fdf39a58330387eb12 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:15:26 +0200 Subject: [PATCH 06/34] Pinned versions --- pom.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 903b8889..d7923178 100644 --- a/pom.xml +++ b/pom.xml @@ -111,7 +111,7 @@ org.slf4j slf4j-simple - 2.0.5 + 2.0.7 test @@ -120,7 +120,9 @@ + org.apache.maven.plugins maven-compiler-plugin + 3.13.0 11 @@ -128,7 +130,9 @@ + org.apache.maven.plugins maven-source-plugin + 3.4.0 attach-sources @@ -140,7 +144,9 @@ + org.apache.maven.plugins maven-javadoc-plugin + 3.12.0 attach-javadocs @@ -161,7 +167,6 @@ maven-surefire-plugin 3.2.5 - ${project.basedir}/test **/*Test.java **/*Tests.java From 20028d8fe09a8025dd9c4c881b11028ff3e25570 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:18:41 +0200 Subject: [PATCH 07/34] Pinned build plugins versions in pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d7923178..d3e31d1c 100644 --- a/pom.xml +++ b/pom.xml @@ -232,7 +232,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.6.3 + 3.12.0 attach-javadocs From cd4a1da94e54aa973bda009ae7942729d8f05a68 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:18:51 +0200 Subject: [PATCH 08/34] Removed empty section in README --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 483e2e51..7efaac5b 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,6 @@ 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 ----------------- From 8f8ade5008770f7ae03d8f11e91360119e767288 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:19:01 +0200 Subject: [PATCH 09/34] Used java.util.logging instead of System.err --- .../github/jopenlibs/vault/VaultConfig.java | 48 +++++++++++-------- .../io/github/jopenlibs/vault/VaultImpl.java | 29 +++++------ 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index f62d55ea..68919fa9 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; /** *

A container for the configuration settings needed to initialize a Vault driver @@ -34,6 +35,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; @@ -369,27 +372,11 @@ public VaultConfig build() throws VaultException { 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.openTimeout == null) { + this.openTimeout = loadTimeoutVariable(VAULT_OPEN_TIMEOUT); } - 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.readTimeout == null) { + this.readTimeout = loadTimeoutVariable(VAULT_READ_TIMEOUT); } if (this.sslConfig == null) { this.sslConfig = new SslConfig().environmentLoader(this.environmentLoader).build(); @@ -398,6 +385,27 @@ public VaultConfig build() throws VaultException { 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 or not an integer + */ + private Integer loadTimeoutVariable(final String name) { + final String value = environmentLoader.loadVariable(name); + if (value == null) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + LOGGER.warning(String.format( + "The %s environment variable contains value \"%s\", which cannot be parsed as an integer timeout period.", + name, value)); + return null; + } + } + public Map getSecretsEnginePathMap() { return Collections.unmodifiableMap(secretsEnginePathMap); } diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index eab96b9a..883b3da6 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -69,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( @@ -99,11 +95,7 @@ public VaultImpl(final VaultConfig vaultConfig, final Integer 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(); } /** @@ -123,11 +115,7 @@ 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()) { try { @@ -146,6 +134,15 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa } } + 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)); + } + } + /** * This method is chained ahead of endpoints (e.g. logical(), auth(), * etc... to specify retry rules for any API operations invoked on that endpoint. @@ -337,7 +334,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; From edbc0259c84c42d2c2a748085529db8cc3f1aabe Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:39:16 +0200 Subject: [PATCH 10/34] Enforced null checks --- .../io/github/jopenlibs/vault/VaultImpl.java | 4 +- .../io/github/jopenlibs/vault/api/Auth.java | 4 +- .../io/github/jopenlibs/vault/api/Debug.java | 4 +- .../github/jopenlibs/vault/api/Logical.java | 10 +-- .../vault/api/database/Database.java | 4 +- .../github/jopenlibs/vault/api/pki/Pki.java | 4 +- .../jopenlibs/vault/api/sys/Leases.java | 4 +- .../github/jopenlibs/vault/api/sys/Seal.java | 4 +- .../github/jopenlibs/vault/api/sys/Sys.java | 4 +- .../jopenlibs/vault/api/sys/Wrapping.java | 4 +- .../vault/response/AuthResponse.java | 24 ++++--- .../vault/response/LookupResponse.java | 24 ++++--- .../vault/response/UnwrapResponse.java | 12 +++- .../vault/response/VaultResponse.java | 8 +++ .../vault/response/WrapResponse.java | 10 ++- .../io/github/jopenlibs/vault/VaultTests.java | 22 +++++++ .../response/ResponseNullHandlingTests.java | 64 +++++++++++++++++++ 17 files changed, 154 insertions(+), 56 deletions(-) create mode 100644 src/test/java/io/github/jopenlibs/vault/response/ResponseNullHandlingTests.java diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 883b3da6..54331bb4 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -88,7 +88,7 @@ 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 + "'."); @@ -117,7 +117,7 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa this.vaultConfig = vaultConfig; logNameSpace(); this.vaultConfig.setEngineVersion(globalFallbackVersion); - if (useSecretsEnginePathMap && this.vaultConfig.getSecretsEnginePathMap().isEmpty()) { + if (Boolean.TRUE.equals(useSecretsEnginePathMap) && this.vaultConfig.getSecretsEnginePathMap().isEmpty()) { try { logger.info( "No secrets Engine version map was supplied, attempting to generate one."); 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..9b2e276a 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) { 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..7a7c48e5 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) { 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..04f724bd 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(); } /** @@ -633,7 +632,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 +648,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/database/Database.java b/src/main/java/io/github/jopenlibs/vault/api/database/Database.java index f7188d96..e4475806 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(); } /** 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..8b818b5e 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(); } /** 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 8265f964..76b99549 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 @@ -25,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) { 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 129cce03..bf3fbb96 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 @@ -25,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) { 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 11d35920..9f3051b6 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 @@ -29,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(); } /** 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/test/java/io/github/jopenlibs/vault/VaultTests.java b/src/test/java/io/github/jopenlibs/vault/VaultTests.java index 16694db4..299b3a9b 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(); 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()); + } +} From cb621b111f4f82c9bc6a8ac73fb6b8bfcc706f46 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 6 Oct 2026 23:44:38 +0200 Subject: [PATCH 11/34] Removed unused calls --- .../io/github/jopenlibs/vault/VaultImpl.java | 3 -- .../io/github/jopenlibs/vault/api/Auth.java | 39 ------------------- .../io/github/jopenlibs/vault/api/Debug.java | 5 +-- .../github/jopenlibs/vault/api/Logical.java | 24 ------------ .../vault/api/database/Database.java | 15 ------- .../github/jopenlibs/vault/api/pki/Pki.java | 15 ------- .../jopenlibs/vault/api/sys/Leases.java | 12 ------ .../github/jopenlibs/vault/api/sys/Seal.java | 9 ----- .../jopenlibs/vault/api/sys/Wrapping.java | 16 +------- .../vault/api/sys/mounts/Mounts.java | 15 ------- 10 files changed, 3 insertions(+), 150 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 54331bb4..9a7df5f9 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -307,10 +307,7 @@ private Map collectSecretEngineVersions() { .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; 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 9b2e276a..32fc259e 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Auth.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Auth.java @@ -378,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 @@ -424,10 +421,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 @@ -500,10 +494,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 @@ -565,10 +556,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 @@ -672,10 +660,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 @@ -728,10 +713,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 @@ -791,10 +773,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 @@ -852,10 +831,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 @@ -921,10 +897,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 @@ -1067,10 +1040,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 @@ -1128,10 +1098,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 @@ -1169,10 +1136,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 @@ -1235,10 +1199,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 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 7a7c48e5..52832f34 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Debug.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Debug.java @@ -107,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 04f724bd..ca7c91e7 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Logical.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Logical.java @@ -91,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 @@ -151,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 @@ -291,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) @@ -400,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 @@ -450,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(); @@ -510,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(); @@ -557,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(); @@ -594,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(); 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 e4475806..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 @@ -95,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 @@ -140,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 @@ -192,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 @@ -238,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 @@ -285,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 8b818b5e..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 @@ -121,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 @@ -168,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 @@ -219,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 @@ -265,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 @@ -529,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 76b99549..0e203dc0 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 @@ -56,10 +56,7 @@ public VaultResponse revoke(final String leaseId) throws VaultException { .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 @@ -96,10 +93,7 @@ public VaultResponse revokePrefix(final String prefix) 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 response @@ -138,10 +132,7 @@ public VaultResponse revokeForce(final String prefix) 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 response @@ -187,10 +178,7 @@ public VaultResponse renew(final String leaseId, final long increment) throws Va .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 bf3fbb96..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 @@ -47,10 +47,7 @@ public SealResponse seal() 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(); return getSealResponse(attempt, restResponse, 204); @@ -88,10 +85,7 @@ public SealResponse unseal(final String key, final Boolean reset) throws VaultEx .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 @@ -112,10 +106,7 @@ public SealResponse sealStatus() throws VaultException { .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/Wrapping.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Wrapping.java index 9f3051b6..59772bf7 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 @@ -112,10 +112,7 @@ public LogicalResponse lookupWrap(final char[] wrappedToken, boolean inBody) .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 @@ -216,10 +213,7 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault .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 @@ -385,10 +379,7 @@ public UnwrapResponse unwrap(final char[] wrappedToken, boolean inBody) throws V .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(); @@ -474,10 +465,7 @@ public WrapResponse rewrap(final char[] wrappedToken) throws VaultException { .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 423d89f7..4baf21d5 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 @@ -47,10 +47,7 @@ public MountResponse list() throws VaultException { .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 @@ -118,10 +115,7 @@ public MountResponse enable(final String path, final MountType type, final Mount .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 @@ -167,10 +161,7 @@ public MountResponse disable(final String path) throws VaultException { .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 @@ -216,10 +207,7 @@ public MountResponse read(final String path) throws VaultException { .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 @@ -281,10 +269,7 @@ public MountResponse tune(final String path, final MountPayload payload) throws .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 From caf0980023dacecf315c7914b0ae68bc5e195a03 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 00:29:30 +0200 Subject: [PATCH 12/34] Improved parsing of VAULT_SSL_VERIFY variable --- .../io/github/jopenlibs/vault/SslConfig.java | 40 +++++++- .../jopenlibs/vault/VaultConfigTests.java | 94 +++++++++++++++++++ 2 files changed, 129 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/SslConfig.java b/src/main/java/io/github/jopenlibs/vault/SslConfig.java index b60b24b1..c3d5e9cc 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,6 +49,8 @@ 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 transient SSLContext sslContext; @@ -90,7 +95,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,12 +501,10 @@ 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.loadVariable(VAULT_SSL_VERIFY)); } if (this.verify && this.pemUTF8 == null && environmentLoader.loadVariable(VAULT_SSL_CERT) != null) { @@ -515,6 +519,32 @@ public SslConfig build() throws VaultException { return this; } + /** + * 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; + } + public boolean isVerify() { return verify; } diff --git a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index afc8c32e..81751eaa 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -8,10 +8,18 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +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 +262,90 @@ 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()); + } } From 3ac83e1b41dc18c72b292e1da13157c7f3fc84a8 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 00:38:13 +0200 Subject: [PATCH 13/34] Fixed empty env variables checks --- .../jopenlibs/vault/EnvironmentLoader.java | 67 ++++++++++----- .../io/github/jopenlibs/vault/SslConfig.java | 18 +++-- .../github/jopenlibs/vault/VaultConfig.java | 6 +- .../vault/EnvironmentLoaderTests.java | 81 +++++++++++++++++++ .../jopenlibs/vault/VaultConfigTests.java | 31 +++++++ 5 files changed, 172 insertions(+), 31 deletions(-) create mode 100644 src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java diff --git a/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java b/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java index 0e2cbfc7..0ea56b6b 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,56 @@ */ public class EnvironmentLoader implements Serializable { + /** + * Loads an environment variable. 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 = nonBlank(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 = nonBlank(Files.readString(userHome().resolve(".vault-token")).strip()); + } 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, treating an empty or blank value as + * unset. The check 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, or null if it is unset, empty or blank + */ + static String load(final EnvironmentLoader loader, final String name) { + return nonBlank(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 nonBlank(final String value) { + return value == null || value.isBlank() ? null : value; + } + } diff --git a/src/main/java/io/github/jopenlibs/vault/SslConfig.java b/src/main/java/io/github/jopenlibs/vault/SslConfig.java index c3d5e9cc..69249066 100644 --- a/src/main/java/io/github/jopenlibs/vault/SslConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/SslConfig.java @@ -504,15 +504,17 @@ public SslConfig build() throws VaultException { if (this.verifyObject != null) { this.verify = verifyObject; } else { - this.verify = parseVerify(environmentLoader.loadVariable(VAULT_SSL_VERIFY)); + 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(); diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index 68919fa9..9d4e405d 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -361,14 +361,14 @@ 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); + String envVaultToken = EnvironmentLoader.load(environmentLoader, VAULT_TOKEN); if (this.token == null && envVaultToken != null) { this.token = envVaultToken.toCharArray(); } @@ -392,7 +392,7 @@ public VaultConfig build() throws VaultException { * @return The timeout, or null if the variable is unset or not an integer */ private Integer loadTimeoutVariable(final String name) { - final String value = environmentLoader.loadVariable(name); + final String value = EnvironmentLoader.load(environmentLoader, name); if (value == null) { return null; } 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..22cc3fb6 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java @@ -0,0 +1,81 @@ +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 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/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index 81751eaa..91f2beb9 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -348,4 +348,35 @@ public void testConfigBuilder_SslVerifyExplicitOverridesEnv() throws VaultExcept 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()); + } } From dbe7c53b40afa03a25a0f5a7f24cb9eb1c4827b9 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 00:46:38 +0200 Subject: [PATCH 14/34] Trimmed env vars --- .../jopenlibs/vault/EnvironmentLoader.java | 21 ++++++++------ .../vault/EnvironmentLoaderTests.java | 8 +++++ .../jopenlibs/vault/VaultConfigTests.java | 29 +++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java b/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java index 0ea56b6b..f80b79e4 100644 --- a/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java +++ b/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java @@ -13,7 +13,8 @@ public class EnvironmentLoader implements Serializable { /** - * Loads an environment variable. An empty or blank value is treated as unset. + * 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.

@@ -22,11 +23,11 @@ public class EnvironmentLoader implements Serializable { * @return The variable value, or null if it is unset */ public String loadVariable(final String name) { - String value = nonBlank(getenv(name)); + 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 = nonBlank(Files.readString(userHome().resolve(".vault-token")).strip()); + value = normalize(Files.readString(userHome().resolve(".vault-token"))); } catch (IOException e) { // No-op... there simply isn't a token value available } @@ -35,15 +36,17 @@ public String loadVariable(final String name) { } /** - * Loads an environment variable with the given loader, treating an empty or blank value as - * unset. The check is done here too, because {@link #loadVariable(String)} may be overridden. + * 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, or null if it is unset, empty or blank + * @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 nonBlank(loader.loadVariable(name)); + return normalize(loader.loadVariable(name)); } /** @@ -60,8 +63,8 @@ Path userHome() { return Paths.get(System.getProperty("user.home")); } - private static String nonBlank(final String value) { - return value == null || value.isBlank() ? null : value; + private static String normalize(final String value) { + return value == null || value.isBlank() ? null : value.strip(); } } diff --git a/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java b/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java index 22cc3fb6..659ba70e 100644 --- a/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java +++ b/src/test/java/io/github/jopenlibs/vault/EnvironmentLoaderTests.java @@ -45,6 +45,14 @@ public void testValueIsReturned() { 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")); diff --git a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index 91f2beb9..0740d0dd 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -379,4 +379,33 @@ public void testConfigBuilder_LoadFromEnv_EmptyValuesAreUnset() throws VaultExce 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); + } + } } From 2868197ba0253329a39bdd086198734969497f26 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 00:58:05 +0200 Subject: [PATCH 15/34] Checked that timeout isn't negative --- .../github/jopenlibs/vault/VaultConfig.java | 30 ++++++++++----- .../io/github/jopenlibs/vault/rest/Rest.java | 10 +++-- .../github/jopenlibs/vault/TimeoutTests.java | 38 +++++++++++++++++++ .../jopenlibs/vault/VaultConfigTests.java | 16 ++++++++ 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index 9d4e405d..ec447c62 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -212,7 +212,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 */ @@ -231,7 +231,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 */ @@ -351,7 +351,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()) { @@ -378,6 +379,12 @@ public VaultConfig build() throws VaultException { 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(); } @@ -389,7 +396,8 @@ public VaultConfig build() throws VaultException { * Reads a timeout period from an environment variable. * * @param name The environment variable name - * @return The timeout, or null if the variable is unset or not an integer + * @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); @@ -397,13 +405,17 @@ private Integer loadTimeoutVariable(final String name) { return null; } try { - return Integer.valueOf(value); + final int timeout = Integer.parseInt(value); + if (timeout >= 0) { + return timeout; + } } catch (NumberFormatException e) { - LOGGER.warning(String.format( - "The %s environment variable contains value \"%s\", which cannot be parsed as an integer timeout period.", - name, value)); - return null; + // 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() { 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 acfb1203..64636b22 100644 --- a/src/main/java/io/github/jopenlibs/vault/rest/Rest.java +++ b/src/main/java/io/github/jopenlibs/vault/rest/Rest.java @@ -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 */ @@ -480,7 +480,8 @@ public static HttpClient newHttpClient(final Integer connectTimeoutSeconds, //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)); } @@ -528,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/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 0740d0dd..e270897c 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -408,4 +408,20 @@ public void testConfigBuilder_LoadFromEnv_ValuesAreTrimmed() throws IOException, 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()); + } } From c3752ac0f9b40d72a2e60ce6a6b17e9513b9b527 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 01:02:25 +0200 Subject: [PATCH 16/34] Checked if VAULT_TOKEN env var is null --- .../github/jopenlibs/vault/VaultConfig.java | 9 ++++--- .../jopenlibs/vault/VaultConfigTests.java | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index ec447c62..5eb67a3d 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -369,9 +369,12 @@ public VaultConfig build() throws VaultException { throw new VaultException("No address is set"); } } - String envVaultToken = EnvironmentLoader.load(environmentLoader, VAULT_TOKEN); - if (this.token == null && envVaultToken != null) { - this.token = envVaultToken.toCharArray(); + 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.openTimeout == null) { this.openTimeout = loadTimeoutVariable(VAULT_OPEN_TIMEOUT); diff --git a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index e270897c..6c93b9f5 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -424,4 +424,29 @@ public void testConfigBuilder_LoadFromEnv_ZeroAndNegativeTimeouts() throws Vault 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")); + } } From 63081a814de9f8c17ab54ca427f29e4f1d2a7a30 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 01:14:40 +0200 Subject: [PATCH 17/34] Enforced SSL verify in build() phase --- .../io/github/jopenlibs/vault/SslConfig.java | 37 ++++++- .../github/jopenlibs/vault/VaultConfig.java | 7 +- .../jopenlibs/vault/VaultConfigTests.java | 97 +++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/SslConfig.java b/src/main/java/io/github/jopenlibs/vault/SslConfig.java index 69249066..f2c45bee 100644 --- a/src/main/java/io/github/jopenlibs/vault/SslConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/SslConfig.java @@ -52,7 +52,7 @@ public class SslConfig implements Serializable { 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; @@ -62,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 @@ -518,9 +519,30 @@ public SslConfig build() throws VaultException { } } 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 @@ -547,7 +569,20 @@ static boolean parseVerify(final String value) { 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; } diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index 5eb67a3d..83ae148c 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -193,6 +193,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 @@ -389,8 +393,9 @@ public VaultConfig build() throws VaultException { 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); this.defaultHttpClient = null; return this; } diff --git a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index 6c93b9f5..f88fb3d9 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -449,4 +449,101 @@ public String loadVariable(final String name) { 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(); + } } From 368a32bf68ce9f62570c134cc4d29fdae6347ee2 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 01:17:28 +0200 Subject: [PATCH 18/34] Restored serialized fields when read object in SSL context --- .../io/github/jopenlibs/vault/SslConfig.java | 2 + .../vault/SslConfigSerializationTests.java | 82 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java diff --git a/src/main/java/io/github/jopenlibs/vault/SslConfig.java b/src/main/java/io/github/jopenlibs/vault/SslConfig.java index f2c45bee..626d0b65 100644 --- a/src/main/java/io/github/jopenlibs/vault/SslConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/SslConfig.java @@ -788,6 +788,8 @@ 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(); try { buildSsl(); } catch (VaultException e) { 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..38864a51 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java @@ -0,0 +1,82 @@ +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.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()); + } + + @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()); + } +} From bd4694eacaf5fd8b34a7428b0651662de44e8f05 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 01:32:00 +0200 Subject: [PATCH 19/34] Updated README --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7efaac5b..0d1745f6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ 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) @@ -82,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: @@ -95,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 @@ -163,7 +202,9 @@ 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. @@ -218,7 +259,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: @@ -302,6 +343,35 @@ 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: + * 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 + * 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 + * 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 + * 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 * **6.2.3**: This release contains the following updates: * Improved Java 11 compatibility * Fixed integration tests with Vault 2.x From 0d9ab442ae942b3a121b56e3b0bc23e6daf5b4d2 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 15:52:14 +0200 Subject: [PATCH 20/34] Fixed query parameters encoding --- .../io/github/jopenlibs/vault/rest/Rest.java | 18 ++++++++-------- .../github/jopenlibs/vault/rest/GetTests.java | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) 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 64636b22..77c37d16 100644 --- a/src/main/java/io/github/jopenlibs/vault/rest/Rest.java +++ b/src/main/java/io/github/jopenlibs/vault/rest/Rest.java @@ -510,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() 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..54b70cfe 100644 --- a/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java +++ b/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java @@ -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.

* From ca868fc1c3278ff15197d0cba5e13c7513b53c3b Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 17:54:23 +0200 Subject: [PATCH 21/34] Set transient a custom HttpClient --- .../github/jopenlibs/vault/VaultConfig.java | 6 ++- .../jopenlibs/vault/VaultConfigTests.java | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index 83ae148c..5c8eef6c 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -49,7 +49,8 @@ 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 HttpClient defaultHttpClient; /** @@ -300,6 +301,9 @@ public VaultConfig prefixPath(String prefixPath) { * 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 */ diff --git a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index f88fb3d9..55311681 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -1,10 +1,15 @@ 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; @@ -546,4 +551,46 @@ public void testBuildFailsOnInvalidPassedSslConfig() throws VaultException { .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()); + } } From 2da3fc9f2eb40bd8a656264a0d08ed5e71becfbe Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 18:02:59 +0200 Subject: [PATCH 22/34] Fixed possible stale of HttpClient if SSlConfig change --- .../github/jopenlibs/vault/VaultConfig.java | 55 +++++++++++++++---- .../jopenlibs/vault/VaultConfigTests.java | 41 ++++++++++++++ 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index 5c8eef6c..bdb64a98 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -5,8 +5,10 @@ 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 @@ -51,7 +53,7 @@ public class VaultConfig implements Serializable { private EnvironmentLoader environmentLoader; // HttpClient is not serializable: a deserialized config falls back to the default client private transient HttpClient httpClient; - private transient volatile HttpClient defaultHttpClient; + private transient volatile DefaultHttpClient defaultHttpClient; /** *

The code used to load environment variables is encapsulated here, so that a mock version @@ -204,7 +206,6 @@ public VaultConfig putSecretsEngineVersionForPath(String path, String version) { */ public VaultConfig sslConfig(final SslConfig sslConfig) { this.sslConfig = sslConfig; - this.defaultHttpClient = null; return this; } @@ -223,7 +224,6 @@ public VaultConfig sslConfig(final SslConfig sslConfig) { */ public VaultConfig openTimeout(final Integer openTimeout) { this.openTimeout = openTimeout; - this.defaultHttpClient = null; return this; } @@ -400,7 +400,6 @@ public VaultConfig build() throws VaultException { this.sslConfig = new SslConfig(); } this.sslConfig.buildIfNeeded(this.environmentLoader); - this.defaultHttpClient = null; return this; } @@ -477,8 +476,13 @@ public int getPrefixPathDepth() { /** *

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 built once and shared by all calls, so connections and - * worker threads are reused.

+ * 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 */ @@ -486,18 +490,45 @@ public HttpClient getHttpClient() { 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) { + if (client == null || !client.matches(openTimeout, verify, sslContext)) { synchronized (this) { client = defaultHttpClient; - if (client == null) { - client = Rest.newHttpClient(openTimeout, - sslConfig == null ? null : sslConfig.isVerify(), - sslConfig == null ? null : sslConfig.getSslContext()); + if (client == null || !client.matches(openTimeout, verify, sslContext)) { + client = new DefaultHttpClient(openTimeout, verify, sslContext); defaultHttpClient = client; } } } - return 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/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index 55311681..0085589d 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -13,6 +13,7 @@ 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; @@ -593,4 +594,44 @@ public void testSerializationWithCustomHttpClient() throws Exception { 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)); + } } From cb1059e6172cfc27b1bb492ebb98363b319f7955 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 18:05:46 +0200 Subject: [PATCH 23/34] Added code style plugin in Maven --- pom.xml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pom.xml b/pom.xml index d3e31d1c..ece485af 100644 --- a/pom.xml +++ b/pom.xml @@ -128,6 +128,29 @@
+ + + 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 From b2844870ab6ce6be938901fbea2d73233323ec19 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 18:07:49 +0200 Subject: [PATCH 24/34] Rearranged imports --- src/test/java/io/github/jopenlibs/vault/RetryTests.java | 2 +- src/test/java/io/github/jopenlibs/vault/SSLTests.java | 4 ++-- .../io/github/jopenlibs/vault/api/AuthBackendAwsTests.java | 4 ++-- .../java/io/github/jopenlibs/vault/api/AuthLookupTest.java | 4 ++-- .../java/io/github/jopenlibs/vault/api/TransitApiTest.java | 4 ++-- .../jopenlibs/vault/api/sys/WrappingLookupWrapTest.java | 4 ++-- .../io/github/jopenlibs/vault/api/sys/WrappingUnwrapTest.java | 4 ++-- .../vault/api/sys/WrappingUnwrapWithoutAuthResponseTest.java | 4 ++-- .../io/github/jopenlibs/vault/api/sys/WrappingWrapTest.java | 4 ++-- src/test/java/io/github/jopenlibs/vault/rest/DeleteTests.java | 2 +- src/test/java/io/github/jopenlibs/vault/rest/GetTests.java | 2 +- src/test/java/io/github/jopenlibs/vault/rest/PostTests.java | 2 +- src/test/java/io/github/jopenlibs/vault/rest/PutTests.java | 2 +- src/test/java/io/github/jopenlibs/vault/util/SSLUtils.java | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) 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/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/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/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/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 54b70cfe..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; 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/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; From 398a326481c401a4f96d5a9da7fc1103c2a814cc Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 18:08:59 +0200 Subject: [PATCH 25/34] Checked if address is null --- .../java/io/github/jopenlibs/vault/VaultConfig.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java index bdb64a98..c480c487 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultConfig.java @@ -121,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); } @@ -255,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; From 83a8fa7d51be984e9378ee868246325d96d2a79d Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 18:09:58 +0200 Subject: [PATCH 26/34] Better null check --- .../io/github/jopenlibs/vault/VaultImpl.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 9a7df5f9..0fc97294 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -118,19 +118,24 @@ public VaultImpl(final VaultConfig vaultConfig, final Boolean useSecretsEnginePa logNameSpace(); this.vaultConfig.setEngineVersion(globalFallbackVersion); 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); } } From 1dd56efc358da8bcf9eff64a3ec021cf35bf36cd Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 18:10:06 +0200 Subject: [PATCH 27/34] Added tests --- .../jopenlibs/vault/VaultConfigTests.java | 27 +++++++++++++++++++ .../io/github/jopenlibs/vault/VaultTests.java | 24 +++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java index 0085589d..d79b1e3c 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultConfigTests.java @@ -634,4 +634,31 @@ public void testDefaultHttpClientFollowsSslConfigChanges() throws Exception { 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 299b3a9b..eec1ca93 100644 --- a/src/test/java/io/github/jopenlibs/vault/VaultTests.java +++ b/src/test/java/io/github/jopenlibs/vault/VaultTests.java @@ -159,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); + } + } } From e43d405370070b44e976123c059345a7d673ba91 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Wed, 7 Oct 2026 18:11:44 +0200 Subject: [PATCH 28/34] Updated README --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d1745f6..6b9bd975 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,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). @@ -356,7 +360,13 @@ patches. * A deserialized `SslConfig` now keeps its settings. Previously it had SSL verification disabled * 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 + 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. @@ -369,9 +379,13 @@ patches. (`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 + unused Travis CI configuration. Checkstyle now runs in the build, and fails it on violations * **6.2.3**: This release contains the following updates: * Improved Java 11 compatibility * Fixed integration tests with Vault 2.x From c13827083c3a385573466d6bcaf8acf53c0169c9 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 8 Oct 2026 10:58:56 +0200 Subject: [PATCH 29/34] Added serialization and checked if object is initialized --- .../jopenlibs/vault/EnvironmentLoader.java | 3 + .../io/github/jopenlibs/vault/SslConfig.java | 6 ++ .../vault/SslConfigSerializationTests.java | 56 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java b/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java index f80b79e4..78614fe4 100644 --- a/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java +++ b/src/main/java/io/github/jopenlibs/vault/EnvironmentLoader.java @@ -12,6 +12,9 @@ */ 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. diff --git a/src/main/java/io/github/jopenlibs/vault/SslConfig.java b/src/main/java/io/github/jopenlibs/vault/SslConfig.java index 626d0b65..11d8fd05 100644 --- a/src/main/java/io/github/jopenlibs/vault/SslConfig.java +++ b/src/main/java/io/github/jopenlibs/vault/SslConfig.java @@ -790,6 +790,12 @@ 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/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java b/src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java index 38864a51..3622bc03 100644 --- a/src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java +++ b/src/test/java/io/github/jopenlibs/vault/SslConfigSerializationTests.java @@ -5,6 +5,7 @@ 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; @@ -46,6 +47,61 @@ 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(); From e70c3062741d63b68c6965ccfa14bafe992e60a2 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 8 Oct 2026 11:00:00 +0200 Subject: [PATCH 30/34] Updated README --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b9bd975..1b12feb5 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,11 @@ To disable SSL certificate verification altogether, set `verify(false)`, or set 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 @@ -357,7 +362,10 @@ patches. * 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 + * 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 From 5ef4ddde4822e4abdefd2edf537d0d9e39bbd872 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 8 Oct 2026 11:48:01 +0200 Subject: [PATCH 31/34] Added serialized 6.2.3 test fixtures --- .../serialized-6.2.3/sslconfig-built.ser | Bin 0 -> 336 bytes .../serialized-6.2.3/sslconfig-unbuilt.ser | Bin 0 -> 277 bytes .../serialized-6.2.3/sslconfig-verify-false.ser | Bin 0 -> 378 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/test/resources/serialized-6.2.3/sslconfig-built.ser create mode 100644 src/test/resources/serialized-6.2.3/sslconfig-unbuilt.ser create mode 100644 src/test/resources/serialized-6.2.3/sslconfig-verify-false.ser 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 0000000000000000000000000000000000000000..4b00553e9d29b740609f71768c074dd0a637ad9a GIT binary patch literal 336 zcmZ|Lu}T9$5C-76fTmH5`UsZdZDS#%i)iD5fisQWUNSl7-0ZBolQW0la|phIuV&#J zc({j%;@=JY{Cv#YJ8Z=ON5pwWa;%attc_wh6YttBIjjL24fm}7#u$y{)=>eW0)3A@Op}=m{Eoe3i3<4*b zSS5@4wx}V0w_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8342c78993e0364f492af241afeedfc357e849c9 GIT binary patch literal 277 zcmZ9GO=?2O%>a)@iaK=%_m6?)p&8v0vH=wylY z8jqWg#YAAJ3sV_ojmuRko?JfF(2HCHo!|y74Fhx)d3MLUL9^MR7kJaeDz>O+i+Z%E v`G5Q_?jNvDg*~oK2p9Id#@#JrPjg>m@G2{CD*~fGjql7BjhkLeC?ER;CevIR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9147badce395232ba09e30d1689facb426404bbf GIT binary patch literal 378 zcmZ{g!Ab)$5Qb+LYA=edzJTCSOfDXT-YR-AU}5#rTeh99S<@sY+1N!8A4AYb@Krqe z7z$oJxos8^#kmdq^UwGH&u`Fi7IuXuh47bEPD*W1Ns&7ecqKi_oIKKMCJKm?q|hD1 zT0kpiHG|$n3RM0S>k-Z`&QA_KY_XCDP9;|b&Ab(=7%j@zcaSjXqYA=mHE$f4 z=2NsD_LN%=*1KyWPrI&1;Iq8MiTAMmgK?;} zM6L!41I9FE?EPQHAAn8#xIK7y9HsyZv%2mt!;@ur(i*?)*!$O~&-WCPF?0f#6*jNF E0Pq!oqW}N^ literal 0 HcmV?d00001 From 5305e4a89cbc96435e55fd441a819d31ecea7a5a Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 8 Oct 2026 11:52:23 +0200 Subject: [PATCH 32/34] Added new versions in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a0dd527..e690a39e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: - 2.0.2 - 2.0.3 - 2.0.4 + - 2.1.0 + - 2.1.1 + - 2.1.2 - latest os: - ubuntu-latest From 89625d61abac68587074d21a516235a9b1f254e6 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Thu, 8 Oct 2026 12:24:27 +0200 Subject: [PATCH 33/34] Removed unmaintained versions and deprecated code --- .github/workflows/ci.yml | 8 ----- README.md | 10 ++++++ .../java/io/github/jopenlibs/vault/Vault.java | 3 ++ .../io/github/jopenlibs/vault/VaultImpl.java | 9 +++++ .../io/github/jopenlibs/vault/api/Auth.java | 10 ++++++ .../jopenlibs/vault/api/sys/Leases.java | 8 ++--- .../jopenlibs/vault/api/sys/Wrapping.java | 36 +++++++++---------- .../vault/api/sys/mounts/Mounts.java | 8 ++--- .../vault/api/AuthBackendAppIdIT.java | 3 ++ .../jopenlibs/vault/api/AuthBackendPkiIT.java | 7 ++++ .../github/jopenlibs/vault/api/LogicalIT.java | 2 ++ .../jopenlibs/vault/util/VaultContainer.java | 4 +++ 12 files changed, 74 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e690a39e..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 diff --git a/README.md b/README.md index 1b12feb5..c4798530 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,16 @@ patches. * 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/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/VaultImpl.java b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java index 0fc97294..04b81768 100644 --- a/src/main/java/io/github/jopenlibs/vault/VaultImpl.java +++ b/src/main/java/io/github/jopenlibs/vault/VaultImpl.java @@ -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); } 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 32fc259e..42017d04 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Auth.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Auth.java @@ -409,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) @@ -1149,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); @@ -1158,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); @@ -1167,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); @@ -1213,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); @@ -1222,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); @@ -1231,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); @@ -1240,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); @@ -1249,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/sys/Leases.java b/src/main/java/io/github/jopenlibs/vault/api/sys/Leases.java index 0e203dc0..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 @@ -38,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());
      * }
*
@@ -77,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());
      * }
*
@@ -116,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());
      * }
*
@@ -150,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());
      * }
*
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 59772bf7..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 @@ -40,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");
      * }
@@ -61,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");
      * }
@@ -85,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");
      * }
@@ -174,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 @@ -187,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 * } *
* @@ -249,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();
      * }
@@ -287,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 @@ -300,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 * } *
* @@ -340,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 @@ -353,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 * } * * @@ -426,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") @@ -438,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 * } * * 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 4baf21d5..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 @@ -85,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(); * } @@ -145,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(); * } @@ -191,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(); * } @@ -245,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(); * } 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/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/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/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); From 0ac59d2e4c70ff0a27632466969a9f4a1ed72c51 Mon Sep 17 00:00:00 2001 From: Enrico Bianchi Date: Tue, 13 Oct 2026 23:41:57 +0200 Subject: [PATCH 34/34] Updated README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c4798530..47971243 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,9 @@ number) should represent non-breaking changes. The third number represents any v 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`,