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 6fe3e06f..ec101317 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Logical.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Logical.java @@ -33,7 +33,7 @@ public class Logical extends OperationsBase { private String nameSpace; - public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, listV1, listV2, readV1, readV2, writeV1, writeV2, unDelete, mount} + public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, listV1, listV2, listSubKeys, readV1, readV2, writeV1, writeV2, unDelete, mount} public Logical(final VaultConfig config) { super(config); @@ -338,7 +338,25 @@ public LogicalResponse list(final String path) throws VaultException { } } - private LogicalResponse list(final String path, final logicalOperations operation) + /** + *

Retrieve a list of keys corresponding to key/value pairs at a given Vault path.

+ * + *

Key values ending with a trailing-slash characters are sub-paths. Running a subsequent + * list() + * call, using the original path appended with this key, will retrieve all secret keys stored at + * that sub-path.

+ * + *

This method returns only the secret keys, not values. To retrieve the actual stored + * value for a key, use read() with the key appended onto the original base + * path.

+ * + * @param path The Vault key value at which to look for secrets (e.g. secret) + * @param operation The Vault operation involved to retrieve list + * @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 LogicalResponse list(final String path, final logicalOperations operation) throws VaultException { LogicalResponse response = null; try { diff --git a/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java b/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java index adc3af28..77af15a6 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java +++ b/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java @@ -102,17 +102,24 @@ public static String adjustPathForList(final String path, int prefixPathDepth, final Logical.logicalOperations operation) { final List pathSegments = getPathSegments(path); final StringBuilder adjustedPath = new StringBuilder(); - if (operation.equals(Logical.logicalOperations.listV2)) { - // Version 2 - adjustedPath.append(addQualifierToPath(pathSegments, prefixPathDepth, "metadata")); - if (path.endsWith("/")) { - adjustedPath.append("/"); - } - } else { - // Version 1 - adjustedPath.append(path); + switch (operation) { + case listV1: + // Version 1 + adjustedPath.append(path).append("?list=true"); + break; + case listV2: + // Version 2 + adjustedPath.append(addQualifierToPath(pathSegments, prefixPathDepth, "metadata")); + if (path.endsWith("/")) { + adjustedPath.append("/"); + } + adjustedPath.append("?list=true"); + break; + case listSubKeys: + // Subkeys in version 2 + adjustedPath.append(addQualifierToPath(pathSegments, prefixPathDepth, "subkeys")); + break; } - adjustedPath.append("?list=true"); return adjustedPath.toString(); } diff --git a/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java b/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java index 678e18d2..f9f58a76 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java @@ -1,6 +1,7 @@ package io.github.jopenlibs.vault.response; import io.github.jopenlibs.vault.api.Logical; +import io.github.jopenlibs.vault.api.Logical.logicalOperations; import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.json.JsonArray; import io.github.jopenlibs.vault.json.JsonObject; @@ -18,14 +19,15 @@ */ public class LogicalResponse extends VaultResponse { - private Map data = new HashMap<>(); - private List listData = new ArrayList<>(); + private final Map data = new HashMap<>(); + private final List listData = new ArrayList<>(); + private final List listSubkeys = new ArrayList<>(); + private final Map dataMetadata = new HashMap<>(); private JsonObject dataObject = null; private String leaseId; private WrapResponse wrapResponse; private Boolean renewable; private Long leaseDuration; - private final Map dataMetadata = new HashMap<>(); /** * @param restResponse The raw HTTP response from Vault. @@ -71,6 +73,10 @@ public DataMetadata getDataMetadata() { return new DataMetadata(dataMetadata); } + public List getListSubkeys() { + return listSubkeys; + } + private void parseMetadataFields() { try { final String jsonString = new String(getRestResponse().getBody(), @@ -98,17 +104,14 @@ private void parseResponseData(final Logical.logicalOperations operation) { parseJsonIntoMap(metadataValue.asObject(), dataMetadata); } } - data = new HashMap<>(); + dataObject = jsonObject.get("data").asObject(); parseJsonIntoMap(dataObject, data); // For list operations convert the array of keys to a list of values if (operation.equals(Logical.logicalOperations.listV1) || operation.equals( Logical.logicalOperations.listV2)) { - if ( - getRestResponse().getStatus() != 404 - && data.get("keys") != null - ) { + if (getRestResponse().getStatus() != 404 && data.get("keys") != null) { final JsonArray keys = Json.parse(data.get("keys")).asArray(); for (int index = 0; index < keys.size(); index++) { @@ -117,6 +120,13 @@ private void parseResponseData(final Logical.logicalOperations operation) { } } + + if (operation.equals(logicalOperations.listSubKeys)) { + if (data.containsKey("subkeys")) { + final var keys = Json.parse(data.get("subkeys")).asObject(); + this.listSubkeys.addAll(keys.names()); + } + } } catch (Exception ignored) { } } 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 718c4014..c90b7775 100644 --- a/src/test/java/io/github/jopenlibs/vault/api/LogicalIT.java +++ b/src/test/java/io/github/jopenlibs/vault/api/LogicalIT.java @@ -3,11 +3,13 @@ import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.api.Logical.logicalOperations; import io.github.jopenlibs.vault.response.AuthResponse; import io.github.jopenlibs.vault.response.DataMetadata; import io.github.jopenlibs.vault.response.LogicalResponse; import io.github.jopenlibs.vault.response.WrapResponse; import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.util.VaultVersion; import java.io.IOException; import java.util.HashMap; import java.util.List; @@ -26,6 +28,7 @@ import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertNotSame; import static junit.framework.TestCase.assertTrue; +import static org.junit.Assume.assumeTrue; /** * Integration tests for the basic (i.e. "logical") Vault API operations. @@ -306,6 +309,27 @@ public void testList() throws VaultException { assertTrue(keys.contains("hello")); } + /** + * Write a secret, and then verify that its key shows up in the list, returning their subkeys + * when we use KV Engine version 2. + * This test works from Vault 1.10.0 and onward + * + * @throws VaultException On error. + */ + @Test + public void testListSubKeys() throws VaultException { + assumeTrue(VaultVersion.greatThan("1.9.10")); + + final Vault vault = container.getRootVault(); + final Map testMap = Map.of("value", "world", "test", "done"); + + vault.logical().write("secret/hello", testMap); + final List keys = vault.logical() + .list("secret/hello", logicalOperations.listSubKeys).getListSubkeys(); + assertTrue(keys.contains("test")); + assertTrue(keys.contains("value")); + } + /** * Write a secret, and then verify that its key shows up in the list, using KV Engine version * 1.