BetterCloud / vault-java-driver

Zero-dependency Java client for HashiCorp's Vault
https://bettercloud.github.io/vault-java-driver/
336 stars 223 forks source link

Allow disabling automatic "data" insertion into v2 path #155

Open llchan opened 5 years ago

llchan commented 5 years ago

Appreciate the work so far to add KV v2 support :+1:

We currently have KV v2 engines mounted at mount points containing slashes, which break the assumption that "data" should be second-from-the-root. I don't think there will be a foolproof way to infer the correct place to insert the "data" segment, unless you enumerate readable mounts or something like that (probably not a great idea). Could we add an option to disable the automatic insertion, and leave it to the user to pass the path in ready-to-go?

rrusu656e74 commented 5 years ago

Is there a workaround for this ?

llchan commented 5 years ago

I'm not aware of any. If you look at the code it is quite hard-coded.

As for a higher-level workaround, my use case is the vault features of jenkins configuration-as-code, and for the time being I wrap my jenkins in a script that pulls secrets from vault and sticks them in env vars. This is not ideal because the secrets are floating around in the env without special treatment.

rrusu656e74 commented 5 years ago

ended up using an older version of client 3.1.0

Xtigyro commented 5 years ago

Any updates on this? I cannot make it work with KVv2. Some help will be highly appreciated.

I have:

[root@localhost vault-java-example]# curl -sk     -H "X-Vault-Token: s.7z4VY4YWzYgXJYAWql9JHAjR"     -X GET     https://127.0.0.1:8200/v1/kv/data/hello | jq
{
  "request_id": "513716aa-3df1-01e4-b889-25fea3207e76",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "data": {
      "foo": "world"
    },
    "metadata": {
      "created_time": "2019-03-03T11:53:34.657119825Z",
      "deletion_time": "",
      "destroyed": false,
      "version": 1
    }
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

And:

public class App
{
    public static void main( String[] args ) throws VaultException
    {
         /* The com.bettercloud.vault driver automatically reads a
          * a number of Environment Variables like VAULT_TOKEN or
          * VAULT_ADDR, you should ensure those are set properly
        */

        final VaultConfig config =
                new VaultConfig()
                .build();
        final Vault vault = new Vault(config);
        try {
        final String value = vault.logical()
                .read("kv/hello")
                .getData().get("data/data");
            System.out.format( "foo key in kv/hello is " + value +"\n");
        } catch(VaultException e) {
          System.out.println("Exception thrown: " + e);
        }
    }
}

However, I cannot read with that code the secret - it returns:

[root@localhost vault-java-example]# java -jar target/java-client-example-1.0-SNAPSHOT-jar-with-dependencies.jar    Constructing a Vault instance with no provided Engine version, defaulting to version 2.
Exception thrown: com.bettercloud.vault.VaultException: Vault responded with HTTP status code: 404
Response body: {"errors":[]}
pshapiro4broad commented 5 years ago

I'm seeing the same problem, after our vault server was updated to 1.0. I've tried using version 4.0.0 and also tried using a VaultConfig with engineVersion(1). This produces a path without the data segment but we're still getting a 404 from the server. We've backed out our Java code and have resorted to running vault in a subshell, but we'd rather not depend on vault being installed everywhere our code runs.

pshapiro4broad commented 5 years ago

Not sure this will help you, but I was able to fix my problem by doing two things:

crash-bandi commented 5 years ago

First time posting on Github, so please forgive any lack of proper etiquette.

I recently started trying to use this driver and I ran into the same issue. Here are the code changes that I made to get KV mounts with an extra prefixs. Apologizes for such verbosity.

Update to VaultConfig class:

...
private String secretsPrefix;
...

...
/**
 * <p>Sets the secrets Engine paths used by Vault.</p>
 *
 * @param prefix prefix of secrets mount.
 *                             prefix: prefix path, value: prefix path.
 *                             Example string: "my/custom/prefix"
 * @return This object, with secrets prefix populated, ready for additional builder-pattern method calls or else finalization with the build() method
 */
public VaultConfig secretsPrefix(final String prefix) {
    this.secretsPrefix = prefix;
    return this;
}
...

...
public String getSecretsPrefix() { return secretsPrefix; }
...

Updates to Logical class:

...
public LogicalResponse read(final String path) throws VaultException {
        if (config.getSecretsPrefix() != null)
            return read(config.getSecretsPrefix(), path);

        if (this.engineVersionForSecretPath(path).equals(2)) {
            return read(path, true, logicalOperations.readV2);
        } else return read(path, true, logicalOperations.readV1);
    }

    /**
     * <p>Basic read operation to retrieve a secret.  A single secret key can map to multiple name-value pairs,
     * which can be retrieved from the response object.  E.g.:</p>
     *
     * <blockquote>
     * <pre>{@code
     * final LogicalResponse response = vault.logical().read("secret/hello");
     *
     * final String value = response.getData().get("value");
     * final String otherValue = response.getData().get("other_value");
     * }</pre>
     * </blockquote>
     *
     * @param prefix The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path The Vault key value from which to read (e.g. <code>secret/hello</code>)
     * @return The response information returned from Vault
     * @throws VaultException If any errors occurs with the REST request (e.g. non-200 status code, invalid JSON payload,
     *                        etc), and the maximum number of retries is exceeded.
     */
    public LogicalResponse read(final String prefix, final String path) throws VaultException {
        if (this.engineVersionForSecretPath(path).equals(2)) {
            return read(prefix, path, true, logicalOperations.readV2);
        } else return read(prefix, path, true, logicalOperations.readV1);
    }
...

...
private LogicalResponse read(final String prefix, final String path, Boolean shouldRetry, final logicalOperations operation)
            throws VaultException {
        int retryCount = 0;
        while (true) {
            try {
                // Make an HTTP request to Vault
                final RestResponse restResponse = new Rest()//NOPMD
                        .url(config.getAddress() + "/v1/" + prefix + "/" + adjustPathForReadOrWrite(path, operation))
                        .header("X-Vault-Token", config.getToken())
                        .optionalHeader("X-Vault-Namespace", this.nameSpace)
                        .connectTimeoutSeconds(config.getOpenTimeout())
                        .readTimeoutSeconds(config.getReadTimeout())
                        .sslVerification(config.getSslConfig().isVerify())
                        .sslContext(config.getSslConfig().getSslContext())
                        .get();

                // Validate response
                if (restResponse.getStatus() != 200) {
                    throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus()
                            + "\nResponse body: " + new String(restResponse.getBody(), StandardCharsets.UTF_8),
                            restResponse.getStatus());
                }

                return new LogicalResponse(restResponse, retryCount, operation);
            } catch (RuntimeException | VaultException | RestException e) {
                if (!shouldRetry)
                    throw new VaultException(e);
                // If there are retries to perform, then pause for the configured interval and then execute the loop again...
                if (retryCount < config.getMaxRetries()) {
                    retryCount++;
                    try {
                        final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
                        Thread.sleep(retryIntervalMilliseconds);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                } else if (e instanceof VaultException) {
                    // ... otherwise, give up.
                    throw (VaultException) e;
                } else {
                    throw new VaultException(e);
                }
            }
        }
    }
...

...
/**
     * <p>Basic read operation to retrieve a specified secret version for KV engine version 2. A single secret key version
     * can map to multiple name-value pairs, which can be retrieved from the response object.  E.g.:</p>
     *
     * <blockquote>
     * <pre>{@code
     * final LogicalResponse response = vault.logical().read("secret/hello", true, 1);
     *
     * final String value = response.getData().get("value");
     * final String otherValue = response.getData().get("other_value");
     * }</pre>
     * </blockquote>
     *
     * @param prefix      The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path        The Vault key value from which to read (e.g. <code>secret/hello</code>
     * @param shouldRetry Whether to try more than once
     * @param version     The Integer version number of the secret to read, e.g. "1"
     * @return The response information returned from Vault
     * @throws VaultException If any errors occurs with the REST request (e.g. non-200 status code, invalid JSON payload,
     *                        etc), and the maximum number of retries is exceeded.
     */
    public LogicalResponse read(final String prefix, final String path, Boolean shouldRetry, final Integer version) throws VaultException {
        if (this.engineVersionForSecretPath(path) != 2) {
            throw new VaultException("Version reads are only supported in KV Engine version 2.");
        }
        int retryCount = 0;
        while (true) {
            try {
                // Make an HTTP request to Vault
                final RestResponse restResponse = new Rest()//NOPMD
                        .url(config.getAddress() + "/v1/" + prefix + "/" + adjustPathForReadOrWrite(path, logicalOperations.readV2))
                        .header("X-Vault-Token", config.getToken())
                        .optionalHeader("X-Vault-Namespace", this.nameSpace)
                        .parameter("version", version.toString())
                        .connectTimeoutSeconds(config.getOpenTimeout())
                        .readTimeoutSeconds(config.getReadTimeout())
                        .sslVerification(config.getSslConfig().isVerify())
                        .sslContext(config.getSslConfig().getSslContext())
                        .get();

                // Validate response
                if (restResponse.getStatus() != 200) {
                    throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus()
                            + "\nResponse body: " + new String(restResponse.getBody(), StandardCharsets.UTF_8),
                            restResponse.getStatus());
                }

                return new LogicalResponse(restResponse, retryCount, logicalOperations.readV2);
            } catch (RuntimeException | VaultException | RestException e) {
                if (!shouldRetry)
                    throw new VaultException(e);
                // If there are retries to perform, then pause for the configured interval and then execute the loop again...
                if (retryCount < config.getMaxRetries()) {
                    retryCount++;
                    try {
                        final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
                        Thread.sleep(retryIntervalMilliseconds);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                } else if (e instanceof VaultException) {
                    // ... otherwise, give up.
                    throw (VaultException) e;
                } else {
                    throw new VaultException(e);
                }
            }
        }
    }
...

...
public LogicalResponse write(final String path, final Map<String, Object> nameValuePairs) throws VaultException {
        if (config.getSecretsPrefix() != null)
            return write(config.getSecretsPrefix(), path, nameValuePairs);

        if (engineVersionForSecretPath(path).equals(2)) {
            return write(path, nameValuePairs, logicalOperations.writeV2);
        } else return write(path, nameValuePairs, logicalOperations.writeV1);
    }

    /**
     * <p>Basic operation to store secrets.  Multiple name value pairs can be stored under the same secret key.
     * E.g.:</p>
     *
     * <blockquote>
     * <pre>{@code
     * final Map<String, String> nameValuePairs = new HashMap<String, Object>();
     * nameValuePairs.put("value", "foo");
     * nameValuePairs.put("other_value", "bar");
     *
     * final LogicalResponse response = vault.logical().write("secret/hello", nameValuePairs);
     * }</pre>
     * </blockquote>
     *
     * <p>The values in these name-value pairs may be booleans, numerics, strings, or nested JSON objects.  However,
     * be aware that this method does not recursively parse any nested structures.  If you wish to write arbitrary
     * JSON objects to Vault... then you should parse them to JSON outside of this method, and pass them here as JSON
     * strings.</p>
     *
     * @param prefix      The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path           The Vault key value to which to write (e.g. <code>secret/hello</code>)
     * @param nameValuePairs Secret name and value pairs to store under this Vault key (can be <code>null</code> for
     *                       writing to keys that do not need or expect any fields to be specified)
     * @return The response information received from Vault
     * @throws VaultException If any errors occurs with the REST request, and the maximum number of retries is exceeded.
     */
    public LogicalResponse write(final String prefix, final String path, final Map<String, Object> nameValuePairs) throws VaultException {
        if (engineVersionForSecretPath(path).equals(2)) {
            return write(prefix, path, nameValuePairs, logicalOperations.writeV2);
        } else return write(prefix, path, nameValuePairs, logicalOperations.writeV1);
    }
...

...
    private LogicalResponse write(final String prefix, final String path, final Map<String, Object> nameValuePairs,
                                  final logicalOperations operation) throws VaultException {
        int retryCount = 0;
        while (true) {
            try {
                JsonObject requestJson = Json.object();
                if (nameValuePairs != null) {
                    for (final Map.Entry<String, Object> pair : nameValuePairs.entrySet()) {
                        final Object value = pair.getValue();
                        if (value == null) {
                            requestJson = requestJson.add(pair.getKey(), (String) null);
                        } else if (value instanceof Boolean) {
                            requestJson = requestJson.add(pair.getKey(), (Boolean) pair.getValue());
                        } else if (value instanceof Integer) {
                            requestJson = requestJson.add(pair.getKey(), (Integer) pair.getValue());
                        } else if (value instanceof Long) {
                            requestJson = requestJson.add(pair.getKey(), (Long) pair.getValue());
                        } else if (value instanceof Float) {
                            requestJson = requestJson.add(pair.getKey(), (Float) pair.getValue());
                        } else if (value instanceof Double) {
                            requestJson = requestJson.add(pair.getKey(), (Double) pair.getValue());
                        } else {
                            requestJson = requestJson.add(pair.getKey(), pair.getValue().toString());
                        }
                    }
                }
                // Make an HTTP request to Vault
                final RestResponse restResponse = new Rest()//NOPMD
                        .url(config.getAddress() + "/v1/" + prefix + "/" + adjustPathForReadOrWrite(path, operation))
                        .body(jsonObjectToWriteFromEngineVersion(operation, requestJson).toString().getBytes(StandardCharsets.UTF_8))
                        .header("X-Vault-Token", config.getToken())
                        .optionalHeader("X-Vault-Namespace", this.nameSpace)
                        .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)
                final int restStatus = restResponse.getStatus();
                if (restStatus == 200 || restStatus == 204) {
                    return new LogicalResponse(restResponse, retryCount, operation);
                } else {
                    throw new VaultException("Expecting HTTP status 204 or 200, but instead receiving " + restStatus
                            + "\nResponse body: " + new String(restResponse.getBody(), StandardCharsets.UTF_8), restStatus);
                }
            } catch (Exception e) {
                // If there are retries to perform, then pause for the configured interval and then execute the loop again...
                if (retryCount < config.getMaxRetries()) {
                    retryCount++;
                    try {
                        final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
                        Thread.sleep(retryIntervalMilliseconds);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                } else if (e instanceof VaultException) {
                    // ... otherwise, give up.
                    throw (VaultException) e;
                } else {
                    throw new VaultException(e);
                }
            }
        }
    }
...

...
    public List<String> list(final String path) throws VaultException {
        if (config.getSecretsPrefix() != null)
            return list(config.getSecretsPrefix(), path);

        if (engineVersionForSecretPath(path).equals(2)) {
            return list(path, logicalOperations.listV2);
        } else return list(path, logicalOperations.listV1);
    }

    /**
     * <p>Retrieve a list of keys corresponding to key/value pairs at a given Vault path.</p>
     *
     * <p>Key values ending with a trailing-slash characters are sub-paths.  Running a subsequent <code>list()</code>
     * call, using the original path appended with this key, will retrieve all secret keys stored at that sub-path.</p>
     *
     * <p>This method returns only the secret keys, not values.  To retrieve the actual stored value for a key,
     * use <code>read()</code> with the key appended onto the original base path.</p>
     *
     * @param prefix The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path The Vault key value at which to look for secrets (e.g. <code>secret</code>)
     * @return A list of keys corresponding to key/value pairs at a given Vault path, or an empty list if there are none
     * @throws VaultException If any errors occur, or unexpected response received from Vault
     */
    public List<String> list(final String prefix, final String path) throws VaultException {
        if (engineVersionForSecretPath(path).equals(2)) {
            return list(prefix, path, logicalOperations.listV2);
        } else return list(prefix, path, logicalOperations.listV1);
    }
...

...
   private List<String> list(final String prefix, final String path, final logicalOperations operation) throws VaultException {
        LogicalResponse response = null;
        try {
            response = read(prefix, adjustPathForList(path, operation), true, operation);
        } catch (final VaultException e) {
            if (e.getHttpStatusCode() != 404) {
                throw e;
            }
        }

        final List<String> returnValues = new ArrayList<>();
        if (
                response != null
                        && response.getRestResponse().getStatus() != 404
                        && response.getData() != null
                        && response.getData().get("keys") != null
        ) {

            final JsonArray keys = Json.parse(response.getData().get("keys")).asArray();
            for (int index = 0; index < keys.size(); index++) {
                returnValues.add(keys.get(index).asString());
            }
        }
        return returnValues;
    }
...

...
    public LogicalResponse delete(final String path) throws VaultException {
        if (config.getSecretsPrefix() != null)
            return delete(config.getSecretsPrefix(), path);

        if (engineVersionForSecretPath(path).equals(2)) {
            return delete(path, logicalOperations.deleteV2);
        } else return delete(path, logicalOperations.deleteV1);
    }

    /**
     * <p>Deletes the key/value pair located at the provided path.</p>
     *
     * <p>If the path represents a sub-path, then all of its contents must be deleted prior to deleting the empty
     * sub-path itself.</p>
     *
     * @param prefix The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path The Vault key value to delete (e.g. <code>secret/hello</code>).
     * @return The response information received from Vault
     * @throws VaultException If any error occurs, or unexpected response received from Vault
     */
    public LogicalResponse delete(final String prefix, final String path) throws VaultException {
        if (engineVersionForSecretPath(path).equals(2)) {
            return delete(prefix, path, logicalOperations.deleteV2);
        } else return delete(prefix, path, logicalOperations.deleteV1);
    }
...

...
    private LogicalResponse delete(final String prefix, final String path, final Logical.logicalOperations operation) throws VaultException {
        int retryCount = 0;
        while (true) {
            try {
                // Make an HTTP request to Vault
                final RestResponse restResponse = new Rest()//NOPMD
                        .url(config.getAddress() + "/v1/" + prefix + "/" + adjustPathForDelete(path, operation))
                        .header("X-Vault-Token", config.getToken())
                        .optionalHeader("X-Vault-Namespace", this.nameSpace)
                        .connectTimeoutSeconds(config.getOpenTimeout())
                        .readTimeoutSeconds(config.getReadTimeout())
                        .sslVerification(config.getSslConfig().isVerify())
                        .sslContext(config.getSslConfig().getSslContext())
                        .delete();

                // Validate response
                if (restResponse.getStatus() != 204) {
                    throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus()
                            + "\nResponse body: " + new String(restResponse.getBody(), StandardCharsets.UTF_8),
                            restResponse.getStatus());
                }
                return new LogicalResponse(restResponse, retryCount, operation);
            } catch (RuntimeException | VaultException | RestException e) {
                // If there are retries to perform, then pause for the configured interval and then execute the loop again...
                if (retryCount < config.getMaxRetries()) {
                    retryCount++;
                    try {
                        final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
                        Thread.sleep(retryIntervalMilliseconds);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                } else if (e instanceof VaultException) {
                    // ... otherwise, give up.
                    throw (VaultException) e;
                } else {
                    throw new VaultException(e);
                }
            }
        }
    }
...

...
    /**
     * <p>Soft deletes the specified version of the key/value pair located at the provided path.</p>
     * <p>
     * Only supported for KV Engine version 2. If the data is desired, it can be recovered with a matching unDelete operation.
     *
     * <p>If the path represents a sub-path, then all of its contents must be deleted prior to deleting the empty
     * sub-path itself.</p>
     *
     * @param prefix The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path     The Vault key value to delete (e.g. <code>secret/hello</code>).
     * @param versions An array of Integers corresponding to the versions you wish to delete, e.g. [1, 2] etc.
     * @return The response information received from Vault
     * @throws VaultException If any error occurs, or unexpected response received from Vault
     */
    public LogicalResponse delete(final String prefix, final String path, final int[] versions) throws VaultException {
        if (this.engineVersionForSecretPath(path) != 2) {
            throw new VaultException("Version deletes are only supported for KV Engine 2.");
        }
        intArrayCheck(versions);
        int retryCount = 0;
        while (true) {
            try {
                // Make an HTTP request to Vault
                JsonObject versionsToDelete = new JsonObject().add("versions", versions);
                final RestResponse restResponse = new Rest()//NOPMD
                        .url(config.getAddress() + "/v1/" + prefix + "/" + adjustPathForVersionDelete(path))
                        .header("X-Vault-Token", config.getToken())
                        .optionalHeader("X-Vault-Namespace", this.nameSpace)
                        .connectTimeoutSeconds(config.getOpenTimeout())
                        .readTimeoutSeconds(config.getReadTimeout())
                        .sslVerification(config.getSslConfig().isVerify())
                        .sslContext(config.getSslConfig().getSslContext())
                        .body(versionsToDelete.toString().getBytes(StandardCharsets.UTF_8))
                        .post();

                // Validate response
                return getLogicalResponse(retryCount, restResponse);
            } catch (RuntimeException | VaultException | RestException e) {
                // If there are retries to perform, then pause for the configured interval and then execute the loop again...
                if (retryCount < config.getMaxRetries()) {
                    retryCount++;
                    try {
                        final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
                        Thread.sleep(retryIntervalMilliseconds);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                } else if (e instanceof VaultException) {
                    // ... otherwise, give up.
                    throw (VaultException) e;
                } else {
                    throw new VaultException(e);
                }
            }
        }
    }
...

...
    public LogicalResponse unDelete(final String path, final int[] versions) throws VaultException {
        if (config.getSecretsPrefix() != null)
            return unDelete(config.getSecretsPrefix(), path, versions);
...

...
    /**
     * <p>Recovers a soft delete of the specified version of the key/value pair located at the provided path.</p>
     * <p>
     * Only supported for KV Engine version 2.
     *
     * @param prefix The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path     The Vault key value to undelete (e.g. <code>secret/hello</code>).
     * @param versions An array of Integers corresponding to the versions you wish to undelete, e.g. [1, 2] etc.
     * @return The response information received from Vault
     * @throws VaultException If any error occurs, or unexpected response received from Vault
     */
    public LogicalResponse unDelete(final String prefix, final String path, final int[] versions) throws VaultException {
        if (this.engineVersionForSecretPath(path) != 2) {
            throw new VaultException("Version undeletes are only supported for KV Engine 2.");
        }
        intArrayCheck(versions);
        int retryCount = 0;
        while (true) {
            try {
                // Make an HTTP request to Vault
                JsonObject versionsToUnDelete = new JsonObject().add("versions", versions);
                final RestResponse restResponse = new Rest()//NOPMD
                        .url(config.getAddress() + "/v1/" + prefix + "/" + adjustPathForVersionUnDelete(path))
                        .header("X-Vault-Token", config.getToken())
                        .optionalHeader("X-Vault-Namespace", this.nameSpace)
                        .connectTimeoutSeconds(config.getOpenTimeout())
                        .readTimeoutSeconds(config.getReadTimeout())
                        .sslVerification(config.getSslConfig().isVerify())
                        .sslContext(config.getSslConfig().getSslContext())
                        .body(versionsToUnDelete.toString().getBytes(StandardCharsets.UTF_8))
                        .post();

                // Validate response
                if (restResponse.getStatus() != 204) {
                    throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus()
                            + "\nResponse body: " + new String(restResponse.getBody(), StandardCharsets.UTF_8),
                            restResponse.getStatus());
                }
                return new LogicalResponse(restResponse, retryCount, logicalOperations.unDelete);
            } catch (RuntimeException | VaultException | RestException e) {
                // If there are retries to perform, then pause for the configured interval and then execute the loop again...
                if (retryCount < config.getMaxRetries()) {
                    retryCount++;
                    try {
                        final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
                        Thread.sleep(retryIntervalMilliseconds);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                } else if (e instanceof VaultException) {
                    // ... otherwise, give up.
                    throw (VaultException) e;
                } else {
                    throw new VaultException(e);
                }
            }
        }
    }
...

...
    public LogicalResponse destroy(final String path, final int[] versions) throws VaultException {
        if (config.getSecretsPrefix() != null)
            return destroy(config.getSecretsPrefix(), path, versions);
...

...
    /**
     * <p>Performs a hard delete of the specified version of the key/value pair located at the provided path.</p>
     * <p>
     * Only supported for KV Engine version 2. There are no recovery options for the specified version of the data deleted
     * in this method.
     *
     * @param prefix The prefix of the secrets mount (e.g. <code>this/is/my/prefix</code>)
     * @param path     The Vault key value to destroy (e.g. <code>secret/hello</code>).
     * @param versions An array of Integers corresponding to the versions you wish to destroy, e.g. [1, 2] etc.
     * @return The response information received from Vault
     * @throws VaultException If any error occurs, or unexpected response received from Vault
     */
    public LogicalResponse destroy(final String prefix, final String path, final int[] versions) throws VaultException {
        if (this.engineVersionForSecretPath(path) != 2) {
            throw new VaultException("Secret destroys are only supported for KV Engine 2.");
        }
        intArrayCheck(versions);
        int retryCount = 0;
        while (true) {
            try {
                // Make an HTTP request to Vault
                JsonObject versionsToDestroy = new JsonObject().add("versions", versions);
                final RestResponse restResponse = new Rest()//NOPMD
                        .url(config.getAddress() + "/v1/" + adjustPathForVersionDestroy(path))
                        .header("X-Vault-Token", config.getToken())
                        .optionalHeader("X-Vault-Namespace", this.nameSpace)
                        .connectTimeoutSeconds(config.getOpenTimeout())
                        .readTimeoutSeconds(config.getReadTimeout())
                        .sslVerification(config.getSslConfig().isVerify())
                        .sslContext(config.getSslConfig().getSslContext())
                        .body(versionsToDestroy.toString().getBytes(StandardCharsets.UTF_8))
                        .post();

                // Validate response
                return getLogicalResponse(retryCount, restResponse);
            } catch (RuntimeException | VaultException | RestException e) {
                // If there are retries to perform, then pause for the configured interval and then execute the loop again...
                if (retryCount < config.getMaxRetries()) {
                    retryCount++;
                    try {
                        final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds();
                        Thread.sleep(retryIntervalMilliseconds);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                } else if (e instanceof VaultException) {
                    // ... otherwise, give up.
                    throw (VaultException) e;
                } else {
                    throw new VaultException(e);
                }
            }
        }
    }
...

Usage example:

        try {
            final SslConfig sslConfig = new SslConfig().verify(false);
            final VaultConfig config = new VaultConfig().engineVersion(2).address("https://127.0.0.1:8200").sslConfig(sslConfig).token("s.abcde12345").secretsPrefix("extra/prefix/stuff").build();
            final Vault vault = new Vault(config);
            final String value = vault.logical().read("secret/foo").getData().get("bar");
            System.out.println(value);
        } catch (VaultException e) {
            e.printStackTrace();
        }
llchan commented 5 years ago

@crash-bandi could you push your changes to a branch in your own fork, and then submit a PR? Even if it's still a work in progress, that would make it easier for people to review the diff.

bgkaiser commented 5 years ago

Similar fix for this problem is now available on pull request #189. Fix is backward-compatible and requires no change for current library users.