Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
llchan opened this issue Feb 21, 2019 · 9 comments
Open

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

llchan opened this issue Feb 21, 2019 · 9 comments

Comments

@llchan
Copy link

llchan commented Feb 21, 2019

Appreciate the work so far to add KV v2 support 👍

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
Copy link

Is there a workaround for this ?

@llchan
Copy link
Author

llchan commented Feb 27, 2019

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
Copy link

ended up using an older version of client 3.1.0

@Xtigyro
Copy link

Xtigyro commented Mar 3, 2019

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
Copy link

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
Copy link

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

  • add engineVersion(1) to VaultConfig
  • change my VAULT_ADDR setting so it doesn't end in a /. (This worked before, and now it doesn't.)

@crash-bandi
Copy link

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
Copy link
Author

llchan commented Aug 9, 2019

@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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants