Skip to content

Commit 871375f

Browse files
Expose CAS option on secret write and version on secret read (K/V 2) (#52)
1 parent 38fad23 commit 871375f

File tree

7 files changed

+306
-47
lines changed

7 files changed

+306
-47
lines changed

src/main/java/io/github/jopenlibs/vault/api/Logical.java

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
*/
3030
public class Logical extends OperationsBase {
3131

32+
private static final WriteOptions DEFAULT_WRITE_OPTIONS = new WriteOptions().build();
33+
3234
private String nameSpace;
3335

3436
public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, listV1, listV2, readV1, readV2, writeV1, writeV2, unDelete, mount}
@@ -201,9 +203,11 @@ public LogicalResponse read(final String path, Boolean shouldRetry, final Intege
201203
public LogicalResponse write(final String path, final Map<String, Object> nameValuePairs)
202204
throws VaultException {
203205
if (engineVersionForSecretPath(path).equals(2)) {
204-
return write(path, nameValuePairs, logicalOperations.writeV2, null);
206+
return write(path, nameValuePairs, logicalOperations.writeV2, null,
207+
DEFAULT_WRITE_OPTIONS);
205208
} else {
206-
return write(path, nameValuePairs, logicalOperations.writeV1, null);
209+
return write(path, nameValuePairs, logicalOperations.writeV1, null,
210+
DEFAULT_WRITE_OPTIONS);
207211
}
208212
}
209213

@@ -238,47 +242,50 @@ public LogicalResponse write(final String path, final Map<String, Object> nameVa
238242
final Integer wrapTTL)
239243
throws VaultException {
240244
if (engineVersionForSecretPath(path).equals(2)) {
241-
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL);
245+
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL,
246+
DEFAULT_WRITE_OPTIONS);
242247
} else {
243-
return write(path, nameValuePairs, logicalOperations.writeV1, wrapTTL);
248+
return write(path, nameValuePairs, logicalOperations.writeV1, wrapTTL,
249+
DEFAULT_WRITE_OPTIONS);
250+
}
251+
}
252+
253+
/**
254+
* <p>Operation to store secrets with the ability to specify additional write options
255+
* See {@link #write(String, Map, Integer) write} for common behavior
256+
* </p>
257+
*
258+
* @param path The Vault key value to which to write (e.g. <code>secret/hello</code>)
259+
* @param nameValuePairs Secret name and value pairs to store under this Vault key (can be
260+
* @param wrapTTL Time (in seconds) which secret is wrapped
261+
* @param writeOptions Additional options to be used for the write operation
262+
* @return The response information received from Vault
263+
* @throws VaultException If invalid engine version or if errors occurs with the REST request,
264+
* and the maximum number of retries is exceeded.
265+
*/
266+
public LogicalResponse write(final String path, final Map<String, Object> nameValuePairs,
267+
final Integer wrapTTL, final WriteOptions writeOptions)
268+
throws VaultException {
269+
if (!this.engineVersionForSecretPath(path).equals(2)) {
270+
throw new VaultException("Write options are only supported in KV Engine version 2.");
244271
}
272+
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL, writeOptions);
245273
}
246274

247275
private LogicalResponse write(final String path, final Map<String, Object> nameValuePairs,
248-
final logicalOperations operation, final Integer wrapTTL) throws VaultException {
276+
final logicalOperations operation, final Integer wrapTTL,
277+
final WriteOptions writeOptions)
278+
throws VaultException {
249279

250280
return retry(attempt -> {
251-
JsonObject requestJson = Json.object();
252-
if (nameValuePairs != null) {
253-
for (final Map.Entry<String, Object> pair : nameValuePairs.entrySet()) {
254-
final Object value = pair.getValue();
255-
if (value == null) {
256-
requestJson = requestJson.add(pair.getKey(), (String) null);
257-
} else if (value instanceof Boolean) {
258-
requestJson = requestJson.add(pair.getKey(), (Boolean) pair.getValue());
259-
} else if (value instanceof Integer) {
260-
requestJson = requestJson.add(pair.getKey(), (Integer) pair.getValue());
261-
} else if (value instanceof Long) {
262-
requestJson = requestJson.add(pair.getKey(), (Long) pair.getValue());
263-
} else if (value instanceof Float) {
264-
requestJson = requestJson.add(pair.getKey(), (Float) pair.getValue());
265-
} else if (value instanceof Double) {
266-
requestJson = requestJson.add(pair.getKey(), (Double) pair.getValue());
267-
} else if (value instanceof JsonValue) {
268-
requestJson = requestJson.add(pair.getKey(),
269-
(JsonValue) pair.getValue());
270-
} else {
271-
requestJson = requestJson.add(pair.getKey(),
272-
pair.getValue().toString());
273-
}
274-
}
275-
}
276-
// Make an HTTP request to Vault
281+
JsonObject dataJson = buildJsonFromMap(nameValuePairs);
282+
JsonObject optionsJson = buildJsonFromMap(writeOptions.getOptionsMap());
283+
// Make an HTTP request to Vault
277284
final RestResponse restResponse = getRest()//NOPMD
278285
.url(config.getAddress() + "/v1/" + adjustPathForReadOrWrite(path,
279286
config.getPrefixPathDepth(), operation))
280-
.body(jsonObjectToWriteFromEngineVersion(operation, requestJson).toString()
281-
.getBytes(StandardCharsets.UTF_8))
287+
.body(jsonObjectToWriteFromEngineVersion(operation, dataJson, optionsJson)
288+
.toString().getBytes(StandardCharsets.UTF_8))
282289
.header("X-Vault-Token", config.getToken())
283290
.header("X-Vault-Namespace", this.nameSpace)
284291
.header("X-Vault-Request", "true")
@@ -607,4 +614,34 @@ private Integer engineVersionForSecretPath(final String secretPath) {
607614
public Integer getEngineVersionForSecretPath(final String path) {
608615
return this.engineVersionForSecretPath(path);
609616
}
617+
618+
private JsonObject buildJsonFromMap(Map<String, Object> nameValuePairs) {
619+
JsonObject jsonObject = Json.object();
620+
if (nameValuePairs != null) {
621+
for (final Map.Entry<String, Object> pair : nameValuePairs.entrySet()) {
622+
final Object value = pair.getValue();
623+
if (value == null) {
624+
jsonObject = jsonObject.add(pair.getKey(), (String) null);
625+
} else if (value instanceof Boolean) {
626+
jsonObject = jsonObject.add(pair.getKey(), (Boolean) pair.getValue());
627+
} else if (value instanceof Integer) {
628+
jsonObject = jsonObject.add(pair.getKey(), (Integer) pair.getValue());
629+
} else if (value instanceof Long) {
630+
jsonObject = jsonObject.add(pair.getKey(), (Long) pair.getValue());
631+
} else if (value instanceof Float) {
632+
jsonObject = jsonObject.add(pair.getKey(), (Float) pair.getValue());
633+
} else if (value instanceof Double) {
634+
jsonObject = jsonObject.add(pair.getKey(), (Double) pair.getValue());
635+
} else if (value instanceof JsonValue) {
636+
jsonObject = jsonObject.add(pair.getKey(),
637+
(JsonValue) pair.getValue());
638+
} else {
639+
jsonObject = jsonObject.add(pair.getKey(),
640+
pair.getValue().toString());
641+
}
642+
}
643+
}
644+
return jsonObject;
645+
}
646+
610647
}

src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,23 @@ public static String adjustPathForVersionDestroy(final String path, final int pr
198198
}
199199

200200
/**
201-
* In version two, when writing a secret, the JSONObject must be nested with "data" as the key.
201+
* In version two, when writing a secret, the JSONObject must be nested with "data" as the key
202+
* and an "options" key may be optionally provided
202203
*
203204
* @param operation The operation being performed, e.g. writeV1, or writeV2.
204205
* @param jsonObject The jsonObject that is going to be written.
206+
* @param optionsJsonObject The options jsonObject that is going to be written or null if none
205207
* @return This jsonObject mutated for the operation.
206208
*/
207209
public static JsonObject jsonObjectToWriteFromEngineVersion(
208-
final Logical.logicalOperations operation, final JsonObject jsonObject) {
210+
final Logical.logicalOperations operation, final JsonObject jsonObject,
211+
final JsonObject optionsJsonObject) {
209212
if (operation.equals(Logical.logicalOperations.writeV2)) {
210213
final JsonObject wrappedJson = new JsonObject();
211214
wrappedJson.add("data", jsonObject);
215+
if (!optionsJsonObject.isEmpty()) {
216+
wrappedJson.add("options", optionsJsonObject);
217+
}
212218
return wrappedJson;
213219
} else {
214220
return jsonObject;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.github.jopenlibs.vault.api;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
7+
/**
8+
* Additional options that may be set as part of K/V V2 write operation.
9+
* Construct instances of this class using a builder pattern, calling setter methods for each
10+
* value and then terminating with a call to build().
11+
*/
12+
public class WriteOptions {
13+
14+
public static final String CHECK_AND_SET_KEY = "cas";
15+
16+
private final Map<String, Object> options = new HashMap<>();
17+
18+
/**
19+
* Enable check and set (CAS) option
20+
* @param version current version of the secret
21+
* @return updated options ready for additional builder-pattern calls or else finalization
22+
* with the build() method
23+
*/
24+
public WriteOptions checkAndSet(Long version) {
25+
return setOption(CHECK_AND_SET_KEY, version);
26+
}
27+
28+
/**
29+
* Set an option to a value
30+
* @param name option name
31+
* @param value option value
32+
* @return updated options ready for additional builder-pattern calls or else finalization
33+
* with the build() method
34+
*/
35+
public WriteOptions setOption(String name, Object value) {
36+
options.put(name, value);
37+
return this;
38+
}
39+
40+
/**
41+
* Finalize the options (terminating method in the builder pattern)
42+
* @return this object, with all available config options parsed and loaded
43+
*/
44+
public WriteOptions build() {
45+
return this;
46+
}
47+
48+
/**
49+
* @return options as a Map
50+
*/
51+
public Map<String, Object> getOptionsMap() {
52+
return Collections.unmodifiableMap(options);
53+
}
54+
55+
/**
56+
* @return true if no options are set, false otherwise
57+
*/
58+
public boolean isEmpty() {
59+
return options.isEmpty();
60+
}
61+
62+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.github.jopenlibs.vault.response;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
6+
/**
7+
* Container for metadata that can be returned with a logical operation response
8+
*/
9+
public class DataMetadata {
10+
11+
public static final String VERSION_KEY = "version";
12+
13+
private final Map<String, String> metadataMap;
14+
15+
public DataMetadata(Map<String, String> metadataMap) {
16+
this.metadataMap = metadataMap;
17+
}
18+
19+
public Long getVersion() {
20+
final String versionString = metadataMap.get(VERSION_KEY);
21+
return (null != versionString) ? Long.valueOf(versionString) : null;
22+
}
23+
24+
public Map<String, String> getMetadataMap() {
25+
return Collections.unmodifiableMap(metadataMap);
26+
}
27+
28+
public boolean isEmpty() {
29+
return metadataMap.isEmpty();
30+
}
31+
32+
}

src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class LogicalResponse extends VaultResponse {
2525
private WrapResponse wrapResponse;
2626
private Boolean renewable;
2727
private Long leaseDuration;
28+
private final Map<String, String> dataMetadata = new HashMap<>();
2829

2930
/**
3031
* @param restResponse The raw HTTP response from Vault.
@@ -66,6 +67,10 @@ public WrapResponse getWrapResponse() {
6667
return wrapResponse;
6768
}
6869

70+
public DataMetadata getDataMetadata() {
71+
return new DataMetadata(dataMetadata);
72+
}
73+
6974
private void parseMetadataFields() {
7075
try {
7176
final String jsonString = new String(getRestResponse().getBody(),
@@ -88,19 +93,15 @@ private void parseResponseData(final Logical.logicalOperations operation) {
8893
JsonObject jsonObject = Json.parse(jsonString).asObject();
8994
if (operation.equals(Logical.logicalOperations.readV2)) {
9095
jsonObject = jsonObject.get("data").asObject();
96+
JsonValue metadataValue = jsonObject.get("metadata");
97+
if (null != metadataValue) {
98+
parseJsonIntoMap(metadataValue.asObject(), dataMetadata);
99+
}
91100
}
92101
data = new HashMap<>();
93102
dataObject = jsonObject.get("data").asObject();
94-
for (final JsonObject.Member member : dataObject) {
95-
final JsonValue jsonValue = member.getValue();
96-
if (jsonValue == null || jsonValue.isNull()) {
97-
continue;
98-
} else if (jsonValue.isString()) {
99-
data.put(member.getName(), jsonValue.asString());
100-
} else {
101-
data.put(member.getName(), jsonValue.toString());
102-
}
103-
}
103+
parseJsonIntoMap(dataObject, data);
104+
104105
// For list operations convert the array of keys to a list of values
105106
if (operation.equals(Logical.logicalOperations.listV1) || operation.equals(
106107
Logical.logicalOperations.listV2)) {
@@ -119,4 +120,18 @@ private void parseResponseData(final Logical.logicalOperations operation) {
119120
} catch (Exception ignored) {
120121
}
121122
}
123+
124+
private void parseJsonIntoMap(final JsonObject jsonObject, final Map<String, String> map) {
125+
for (final JsonObject.Member member : jsonObject) {
126+
final JsonValue jsonValue = member.getValue();
127+
if (jsonValue == null || jsonValue.isNull()) {
128+
continue;
129+
} else if (jsonValue.isString()) {
130+
map.put(member.getName(), jsonValue.asString());
131+
} else {
132+
map.put(member.getName(), jsonValue.toString());
133+
}
134+
}
135+
}
136+
122137
}

0 commit comments

Comments
 (0)