Skip to content

Commit

Permalink
Expose CAS option on secret write and version on secret read (K/V 2) (j…
Browse files Browse the repository at this point in the history
  • Loading branch information
trailrunner-taulia authored Aug 28, 2023
1 parent 38fad23 commit 871375f
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 47 deletions.
103 changes: 70 additions & 33 deletions src/main/java/io/github/jopenlibs/vault/api/Logical.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
*/
public class Logical extends OperationsBase {

private static final WriteOptions DEFAULT_WRITE_OPTIONS = new WriteOptions().build();

private String nameSpace;

public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, listV1, listV2, readV1, readV2, writeV1, writeV2, unDelete, mount}
Expand Down Expand Up @@ -201,9 +203,11 @@ public LogicalResponse read(final String path, Boolean shouldRetry, final Intege
public LogicalResponse write(final String path, final Map<String, Object> nameValuePairs)
throws VaultException {
if (engineVersionForSecretPath(path).equals(2)) {
return write(path, nameValuePairs, logicalOperations.writeV2, null);
return write(path, nameValuePairs, logicalOperations.writeV2, null,
DEFAULT_WRITE_OPTIONS);
} else {
return write(path, nameValuePairs, logicalOperations.writeV1, null);
return write(path, nameValuePairs, logicalOperations.writeV1, null,
DEFAULT_WRITE_OPTIONS);
}
}

Expand Down Expand Up @@ -238,47 +242,50 @@ public LogicalResponse write(final String path, final Map<String, Object> nameVa
final Integer wrapTTL)
throws VaultException {
if (engineVersionForSecretPath(path).equals(2)) {
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL);
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL,
DEFAULT_WRITE_OPTIONS);
} else {
return write(path, nameValuePairs, logicalOperations.writeV1, wrapTTL);
return write(path, nameValuePairs, logicalOperations.writeV1, wrapTTL,
DEFAULT_WRITE_OPTIONS);
}
}

/**
* <p>Operation to store secrets with the ability to specify additional write options
* See {@link #write(String, Map, Integer) write} for common behavior
* </p>
*
* @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
* @param wrapTTL Time (in seconds) which secret is wrapped
* @param writeOptions Additional options to be used for the write operation
* @return The response information received from Vault
* @throws VaultException If invalid engine version or if errors occurs with the REST request,
* and the maximum number of retries is exceeded.
*/
public LogicalResponse write(final String path, final Map<String, Object> nameValuePairs,
final Integer wrapTTL, final WriteOptions writeOptions)
throws VaultException {
if (!this.engineVersionForSecretPath(path).equals(2)) {
throw new VaultException("Write options are only supported in KV Engine version 2.");
}
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL, writeOptions);
}

private LogicalResponse write(final String path, final Map<String, Object> nameValuePairs,
final logicalOperations operation, final Integer wrapTTL) throws VaultException {
final logicalOperations operation, final Integer wrapTTL,
final WriteOptions writeOptions)
throws VaultException {

return retry(attempt -> {
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 if (value instanceof JsonValue) {
requestJson = requestJson.add(pair.getKey(),
(JsonValue) pair.getValue());
} else {
requestJson = requestJson.add(pair.getKey(),
pair.getValue().toString());
}
}
}
// Make an HTTP request to Vault
JsonObject dataJson = buildJsonFromMap(nameValuePairs);
JsonObject optionsJson = buildJsonFromMap(writeOptions.getOptionsMap());
// Make an HTTP request to Vault
final RestResponse restResponse = getRest()//NOPMD
.url(config.getAddress() + "/v1/" + adjustPathForReadOrWrite(path,
config.getPrefixPathDepth(), operation))
.body(jsonObjectToWriteFromEngineVersion(operation, requestJson).toString()
.getBytes(StandardCharsets.UTF_8))
.body(jsonObjectToWriteFromEngineVersion(operation, dataJson, optionsJson)
.toString().getBytes(StandardCharsets.UTF_8))
.header("X-Vault-Token", config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
Expand Down Expand Up @@ -607,4 +614,34 @@ private Integer engineVersionForSecretPath(final String secretPath) {
public Integer getEngineVersionForSecretPath(final String path) {
return this.engineVersionForSecretPath(path);
}

private JsonObject buildJsonFromMap(Map<String, Object> nameValuePairs) {
JsonObject jsonObject = Json.object();
if (nameValuePairs != null) {
for (final Map.Entry<String, Object> pair : nameValuePairs.entrySet()) {
final Object value = pair.getValue();
if (value == null) {
jsonObject = jsonObject.add(pair.getKey(), (String) null);
} else if (value instanceof Boolean) {
jsonObject = jsonObject.add(pair.getKey(), (Boolean) pair.getValue());
} else if (value instanceof Integer) {
jsonObject = jsonObject.add(pair.getKey(), (Integer) pair.getValue());
} else if (value instanceof Long) {
jsonObject = jsonObject.add(pair.getKey(), (Long) pair.getValue());
} else if (value instanceof Float) {
jsonObject = jsonObject.add(pair.getKey(), (Float) pair.getValue());
} else if (value instanceof Double) {
jsonObject = jsonObject.add(pair.getKey(), (Double) pair.getValue());
} else if (value instanceof JsonValue) {
jsonObject = jsonObject.add(pair.getKey(),
(JsonValue) pair.getValue());
} else {
jsonObject = jsonObject.add(pair.getKey(),
pair.getValue().toString());
}
}
}
return jsonObject;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -198,17 +198,23 @@ public static String adjustPathForVersionDestroy(final String path, final int pr
}

/**
* In version two, when writing a secret, the JSONObject must be nested with "data" as the key.
* In version two, when writing a secret, the JSONObject must be nested with "data" as the key
* and an "options" key may be optionally provided
*
* @param operation The operation being performed, e.g. writeV1, or writeV2.
* @param jsonObject The jsonObject that is going to be written.
* @param optionsJsonObject The options jsonObject that is going to be written or null if none
* @return This jsonObject mutated for the operation.
*/
public static JsonObject jsonObjectToWriteFromEngineVersion(
final Logical.logicalOperations operation, final JsonObject jsonObject) {
final Logical.logicalOperations operation, final JsonObject jsonObject,
final JsonObject optionsJsonObject) {
if (operation.equals(Logical.logicalOperations.writeV2)) {
final JsonObject wrappedJson = new JsonObject();
wrappedJson.add("data", jsonObject);
if (!optionsJsonObject.isEmpty()) {
wrappedJson.add("options", optionsJsonObject);
}
return wrappedJson;
} else {
return jsonObject;
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/io/github/jopenlibs/vault/api/WriteOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.github.jopenlibs.vault.api;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* Additional options that may be set as part of K/V V2 write operation.
* Construct instances of this class using a builder pattern, calling setter methods for each
* value and then terminating with a call to build().
*/
public class WriteOptions {

public static final String CHECK_AND_SET_KEY = "cas";

private final Map<String, Object> options = new HashMap<>();

/**
* Enable check and set (CAS) option
* @param version current version of the secret
* @return updated options ready for additional builder-pattern calls or else finalization
* with the build() method
*/
public WriteOptions checkAndSet(Long version) {
return setOption(CHECK_AND_SET_KEY, version);
}

/**
* Set an option to a value
* @param name option name
* @param value option value
* @return updated options ready for additional builder-pattern calls or else finalization
* with the build() method
*/
public WriteOptions setOption(String name, Object value) {
options.put(name, value);
return this;
}

/**
* Finalize the options (terminating method in the builder pattern)
* @return this object, with all available config options parsed and loaded
*/
public WriteOptions build() {
return this;
}

/**
* @return options as a Map
*/
public Map<String, Object> getOptionsMap() {
return Collections.unmodifiableMap(options);
}

/**
* @return true if no options are set, false otherwise
*/
public boolean isEmpty() {
return options.isEmpty();
}

}
32 changes: 32 additions & 0 deletions src/main/java/io/github/jopenlibs/vault/response/DataMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.github.jopenlibs.vault.response;

import java.util.Collections;
import java.util.Map;

/**
* Container for metadata that can be returned with a logical operation response
*/
public class DataMetadata {

public static final String VERSION_KEY = "version";

private final Map<String, String> metadataMap;

public DataMetadata(Map<String, String> metadataMap) {
this.metadataMap = metadataMap;
}

public Long getVersion() {
final String versionString = metadataMap.get(VERSION_KEY);
return (null != versionString) ? Long.valueOf(versionString) : null;
}

public Map<String, String> getMetadataMap() {
return Collections.unmodifiableMap(metadataMap);
}

public boolean isEmpty() {
return metadataMap.isEmpty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class LogicalResponse extends VaultResponse {
private WrapResponse wrapResponse;
private Boolean renewable;
private Long leaseDuration;
private final Map<String, String> dataMetadata = new HashMap<>();

/**
* @param restResponse The raw HTTP response from Vault.
Expand Down Expand Up @@ -66,6 +67,10 @@ public WrapResponse getWrapResponse() {
return wrapResponse;
}

public DataMetadata getDataMetadata() {
return new DataMetadata(dataMetadata);
}

private void parseMetadataFields() {
try {
final String jsonString = new String(getRestResponse().getBody(),
Expand All @@ -88,19 +93,15 @@ private void parseResponseData(final Logical.logicalOperations operation) {
JsonObject jsonObject = Json.parse(jsonString).asObject();
if (operation.equals(Logical.logicalOperations.readV2)) {
jsonObject = jsonObject.get("data").asObject();
JsonValue metadataValue = jsonObject.get("metadata");
if (null != metadataValue) {
parseJsonIntoMap(metadataValue.asObject(), dataMetadata);
}
}
data = new HashMap<>();
dataObject = jsonObject.get("data").asObject();
for (final JsonObject.Member member : dataObject) {
final JsonValue jsonValue = member.getValue();
if (jsonValue == null || jsonValue.isNull()) {
continue;
} else if (jsonValue.isString()) {
data.put(member.getName(), jsonValue.asString());
} else {
data.put(member.getName(), jsonValue.toString());
}
}
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)) {
Expand All @@ -119,4 +120,18 @@ private void parseResponseData(final Logical.logicalOperations operation) {
} catch (Exception ignored) {
}
}

private void parseJsonIntoMap(final JsonObject jsonObject, final Map<String, String> map) {
for (final JsonObject.Member member : jsonObject) {
final JsonValue jsonValue = member.getValue();
if (jsonValue == null || jsonValue.isNull()) {
continue;
} else if (jsonValue.isString()) {
map.put(member.getName(), jsonValue.asString());
} else {
map.put(member.getName(), jsonValue.toString());
}
}
}

}
Loading

0 comments on commit 871375f

Please sign in to comment.