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.