Skip to content

Commit 3357456

Browse files
trailrunner-tauliaGrégoire Rolland
authored andcommitted
Expose CAS option on secret write and version on secret read (K/V 2) (jopenlibs#52)
(cherry picked from commit 871375f)
1 parent 3042f60 commit 3357456

File tree

7 files changed

+314
-56
lines changed

7 files changed

+314
-56
lines changed

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

Lines changed: 78 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import io.github.jopenlibs.vault.json.JsonObject;
77
import io.github.jopenlibs.vault.json.JsonValue;
88
import io.github.jopenlibs.vault.response.LogicalResponse;
9-
import io.github.jopenlibs.vault.rest.Rest;
109
import io.github.jopenlibs.vault.rest.RestResponse;
1110
import java.nio.charset.StandardCharsets;
1211
import java.util.Arrays;
@@ -30,6 +29,8 @@
3029
*/
3130
public class Logical extends OperationsBase {
3231

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

3536
public enum logicalOperations {authentication, deleteV1, deleteV2, destroy, listV1, listV2, readV1, readV2, writeV1, writeV2, unDelete, mount}
@@ -85,7 +86,7 @@ private LogicalResponse read(final String path, final logicalOperations operatio
8586
throws VaultException {
8687
return retry(attempt -> {
8788
// Make an HTTP request to Vault
88-
final RestResponse restResponse = new Rest()//NOPMD
89+
final RestResponse restResponse = getRest()//NOPMD
8990
.url(config.getAddress() + "/v1/" + adjustPathForReadOrWrite(path,
9091
config.getPrefixPathDepth(), operation))
9192
.header("X-Vault-Token", config.getToken())
@@ -142,7 +143,7 @@ public LogicalResponse read(final String path, Boolean shouldRetry, final Intege
142143
attempt -> {
143144
// Make an HTTP request to Vault
144145
final RestResponse restResponse =
145-
new Rest() //NOPMD
146+
getRest() //NOPMD
146147
.url(config.getAddress() + "/v1/" + adjustPathForReadOrWrite(
147148
path,
148149
config.getPrefixPathDepth(), logicalOperations.readV2))
@@ -202,9 +203,11 @@ public LogicalResponse read(final String path, Boolean shouldRetry, final Intege
202203
public LogicalResponse write(final String path, final Map<String, Object> nameValuePairs)
203204
throws VaultException {
204205
if (engineVersionForSecretPath(path).equals(2)) {
205-
return write(path, nameValuePairs, logicalOperations.writeV2, null);
206+
return write(path, nameValuePairs, logicalOperations.writeV2, null,
207+
DEFAULT_WRITE_OPTIONS);
206208
} else {
207-
return write(path, nameValuePairs, logicalOperations.writeV1, null);
209+
return write(path, nameValuePairs, logicalOperations.writeV1, null,
210+
DEFAULT_WRITE_OPTIONS);
208211
}
209212
}
210213

@@ -239,47 +242,50 @@ public LogicalResponse write(final String path, final Map<String, Object> nameVa
239242
final Integer wrapTTL)
240243
throws VaultException {
241244
if (engineVersionForSecretPath(path).equals(2)) {
242-
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL);
245+
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL,
246+
DEFAULT_WRITE_OPTIONS);
243247
} else {
244-
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.");
245271
}
272+
return write(path, nameValuePairs, logicalOperations.writeV2, wrapTTL, writeOptions);
246273
}
247274

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

251280
return retry(attempt -> {
252-
JsonObject requestJson = Json.object();
253-
if (nameValuePairs != null) {
254-
for (final Map.Entry<String, Object> pair : nameValuePairs.entrySet()) {
255-
final Object value = pair.getValue();
256-
if (value == null) {
257-
requestJson = requestJson.add(pair.getKey(), (String) null);
258-
} else if (value instanceof Boolean) {
259-
requestJson = requestJson.add(pair.getKey(), (Boolean) pair.getValue());
260-
} else if (value instanceof Integer) {
261-
requestJson = requestJson.add(pair.getKey(), (Integer) pair.getValue());
262-
} else if (value instanceof Long) {
263-
requestJson = requestJson.add(pair.getKey(), (Long) pair.getValue());
264-
} else if (value instanceof Float) {
265-
requestJson = requestJson.add(pair.getKey(), (Float) pair.getValue());
266-
} else if (value instanceof Double) {
267-
requestJson = requestJson.add(pair.getKey(), (Double) pair.getValue());
268-
} else if (value instanceof JsonValue) {
269-
requestJson = requestJson.add(pair.getKey(),
270-
(JsonValue) pair.getValue());
271-
} else {
272-
requestJson = requestJson.add(pair.getKey(),
273-
pair.getValue().toString());
274-
}
275-
}
276-
}
277-
// Make an HTTP request to Vault
278-
final RestResponse restResponse = new Rest()//NOPMD
281+
JsonObject dataJson = buildJsonFromMap(nameValuePairs);
282+
JsonObject optionsJson = buildJsonFromMap(writeOptions.getOptionsMap());
283+
// Make an HTTP request to Vault
284+
final RestResponse restResponse = getRest()//NOPMD
279285
.url(config.getAddress() + "/v1/" + adjustPathForReadOrWrite(path,
280286
config.getPrefixPathDepth(), operation))
281-
.body(jsonObjectToWriteFromEngineVersion(operation, requestJson).toString()
282-
.getBytes(StandardCharsets.UTF_8))
287+
.body(jsonObjectToWriteFromEngineVersion(operation, dataJson, optionsJson)
288+
.toString().getBytes(StandardCharsets.UTF_8))
283289
.header("X-Vault-Token", config.getToken())
284290
.header("X-Vault-Namespace", this.nameSpace)
285291
.header("X-Vault-Request", "true")
@@ -368,7 +374,7 @@ private LogicalResponse delete(final String path, final Logical.logicalOperation
368374
throws VaultException {
369375
return retry(attempt -> {
370376
// Make an HTTP request to Vault
371-
final RestResponse restResponse = new Rest()//NOPMD
377+
final RestResponse restResponse = getRest()//NOPMD
372378
.url(config.getAddress() + "/v1/" + adjustPathForDelete(path,
373379
config.getPrefixPathDepth(), operation))
374380
.header("X-Vault-Token", config.getToken())
@@ -418,7 +424,7 @@ public LogicalResponse delete(final String path, final int[] versions) throws Va
418424
return retry(attempt -> {
419425
// Make an HTTP request to Vault
420426
JsonObject versionsToDelete = new JsonObject().add("versions", versions);
421-
final RestResponse restResponse = new Rest()//NOPMD
427+
final RestResponse restResponse = getRest()//NOPMD
422428
.url(config.getAddress() + "/v1/" + adjustPathForVersionDelete(path,
423429
config.getPrefixPathDepth()))
424430
.header("X-Vault-Token", config.getToken())
@@ -478,7 +484,7 @@ public LogicalResponse unDelete(final String path, final int[] versions) throws
478484
return retry(attempt -> {
479485
// Make an HTTP request to Vault
480486
JsonObject versionsToUnDelete = new JsonObject().add("versions", versions);
481-
final RestResponse restResponse = new Rest() //NOPMD
487+
final RestResponse restResponse = getRest() //NOPMD
482488
.url(config.getAddress() + "/v1/" + adjustPathForVersionUnDelete(path,
483489
config.getPrefixPathDepth()))
484490
.header("X-Vault-Token", config.getToken())
@@ -525,7 +531,7 @@ public LogicalResponse destroy(final String path, final int[] versions) throws V
525531
return retry(attempt -> {
526532
// Make an HTTP request to Vault
527533
JsonObject versionsToDestroy = new JsonObject().add("versions", versions);
528-
final RestResponse restResponse = new Rest()//NOPMD
534+
final RestResponse restResponse = getRest()//NOPMD
529535
.url(config.getAddress() + "/v1/" + adjustPathForVersionDestroy(path,
530536
config.getPrefixPathDepth()))
531537
.header("X-Vault-Token", config.getToken())
@@ -562,7 +568,7 @@ public LogicalResponse upgrade(final String kvPath) throws VaultException {
562568
// Make an HTTP request to Vault
563569
JsonObject kvToUpgrade = new JsonObject().add("options",
564570
new JsonObject().add("version", 2));
565-
final RestResponse restResponse = new Rest()//NOPMD
571+
final RestResponse restResponse = getRest()//NOPMD
566572
.url(config.getAddress() + "/v1/sys/mounts/" + (kvPath.replaceAll("/", "")
567573
+ "/tune"))
568574
.header("X-Vault-Token", config.getToken())
@@ -608,4 +614,34 @@ private Integer engineVersionForSecretPath(final String secretPath) {
608614
public Integer getEngineVersionForSecretPath(final String path) {
609615
return this.engineVersionForSecretPath(path);
610616
}
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+
611647
}

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)