Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add operations for interacting with kv metadata #561

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,34 @@
package org.springframework.vault.core;

import org.springframework.vault.support.VaultMetadataRequest;
import org.springframework.vault.support.VaultMetadataResponse;

/**
* Interface that specifies kv metadata related operations
*
* @author Zakaria Amine
* @see <a href="https://www.vaultproject.io/api-docs/secret/kv/kv-v2#update-metadata">kv backend metadata api docs</a>
*/
public interface VaultKeyValueMetadataOperations {

/**
* permanently deletes the key metadata and all version data for the specified key. All version history will be removed.
* @param path the secret path, must not be null or empty
*/
void delete(String path);

/**
* retrieves the metadata and versions for the secret at the specified path.
* @param path the secret path, must not be null or empty
* @return {@link VaultMetadataResponse}
*/
VaultMetadataResponse get(String path);

/**
* Updates the secret metadata, or creates new metadata if not present.
*
* @param path the secret path, must not be null or empty
* @param body {@link VaultMetadataRequest}
*/
void put(String path, VaultMetadataRequest body);
}
@@ -0,0 +1,107 @@
package org.springframework.vault.core;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.springframework.util.Assert;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultMetadataRequest;
import org.springframework.vault.support.VaultMetadataResponse;
import org.springframework.vault.support.Versioned;
import org.springframework.web.client.HttpStatusCodeException;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations {

private final VaultOperations vaultOperations;

private final String basePath;

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

public VaultKeyValueMetadataTemplate(VaultOperations vaultOperations, String basePath) {
Assert.notNull(vaultOperations, "VaultOperations must not be null");
this.vaultOperations = vaultOperations;
this.basePath = basePath;
}

@Override
public void delete(String path) {
Assert.hasText(path, "Path must not be empty");
vaultOperations.delete("/"+this.basePath+"/metadata/" + path);
}

@Override
public VaultMetadataResponse get(String path) {
Assert.hasText(path, "Path must not be empty");
Map<String, Object> metadataResponse =
vaultOperations.read("/" + this.basePath + "/metadata/" + path, Map.class).getData();

return fromMap(metadataResponse);
}

@Override
public void put(String path, VaultMetadataRequest body) {
Assert.hasText(path, "Path must not be empty");
Assert.notNull(body, "Body must not be null");
vaultOperations.doWithSession(restOperations -> {
try {
restOperations.put("/"+this.basePath+"/metadata/" + path, body);
return null;
}
catch (HttpStatusCodeException e) {
throw VaultResponses.buildException(e, path);
}
});
}

private VaultMetadataResponse fromMap(Map<String, Object> metadataResponse) {
return VaultMetadataResponse.builder()
.casRequired(Boolean.parseBoolean(String.valueOf(metadataResponse.get("cas_required"))))
.createdTime(toInstant(metadataResponse.get("created_time")))
.currentVersion(Integer.parseInt(String.valueOf(metadataResponse.get("current_version"))))
.deleteVersionAfter(String.valueOf(metadataResponse.get("delete_version_after")))
.maxVersions(Integer.parseInt(String.valueOf(metadataResponse.get("max_versions"))))
.oldestVersion(Integer.parseInt(String.valueOf(metadataResponse.get("oldest_version"))))
.updatedTime(toInstant(metadataResponse.get("updated_time")))
.versions(buildVersions(metadataResponse.get("versions")))
.build();
}

private static List<Versioned.Metadata> buildVersions(Object versions) {
try {
JsonNode kvVersions = OBJECT_MAPPER.readTree(OBJECT_MAPPER.writeValueAsString(versions));

return StreamSupport.stream(Spliterators.spliteratorUnknownSize(kvVersions.fieldNames(), Spliterator.DISTINCT), false)
.map(version -> fromJsonNode(kvVersions.get(version), version))
.collect(Collectors.toList());
}
catch (Exception e) {
e.printStackTrace();
return new ArrayList<>();
}
}

private static Versioned.Metadata fromJsonNode(JsonNode versionData, String version) {
Instant createdTime = toInstant(versionData.get("created_time").asText());
Instant deletionTime = Objects.equals(versionData.get("deletion_time").asText(), "") ? null : toInstant(versionData.get("deletion_time").asText());
boolean destroyed = versionData.get("destroyed").asBoolean();
Versioned.Version kvVersion = Versioned.Version.from(Integer.parseInt(version));

return Versioned.Metadata.builder().createdAt(createdTime).deletedAt(deletionTime).destroyed(destroyed).version(kvVersion).build();
}

private static Instant toInstant(Object date) {
return Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(String.valueOf(date)));
}
}
Expand Up @@ -120,4 +120,11 @@ default <T> Versioned<T> get(String path, Class<T> responseType) {
* @param versionsToDelete must not be {@literal null} or empty.
*/
void destroy(String path, Version... versionsToDelete);

/**
* Return {@link VaultKeyValueMetadataOperations}
*
* @return the operations interface to interact with the Vault Key/Value metadata backend
*/
VaultKeyValueMetadataOperations opsForKeyValueMetadata();
}
Expand Up @@ -53,6 +53,8 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor

private final VaultOperations vaultOperations;

private final String path;

/**
* Create a new {@link VaultVersionedKeyValueTemplate} given {@link VaultOperations}
* and the mount {@code path}.
Expand All @@ -65,6 +67,7 @@ public VaultVersionedKeyValueTemplate(VaultOperations vaultOperations, String pa
super(vaultOperations, path);

this.vaultOperations = vaultOperations;
this.path = path;
}

@Nullable
Expand Down Expand Up @@ -241,6 +244,11 @@ public void destroy(String path, Version... versionsToDelete) {
Collections.singletonMap("versions", versions));
}

@Override
public VaultKeyValueMetadataOperations opsForKeyValueMetadata() {
return new VaultKeyValueMetadataTemplate(vaultOperations, path);
}

private static class VersionedResponse
extends VaultResponseSupport<VaultResponseSupport<JsonNode>> {
}
Expand Down
@@ -0,0 +1,101 @@
package org.springframework.vault.support;

import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Value object to bind Vault HTTP kv metadata update API requests.
*
* @author Zakaria Amine
* @see <a href="https://www.vaultproject.io/api-docs/secret/kv/kv-v2#update-metadata">Update Metadata</a>
*/
public class VaultMetadataRequest {

@JsonProperty("max_versions")
private int maxVersions;

@JsonProperty("cas_required")
private boolean casRequired;

@JsonProperty("delete_version_after")
private String deleteVersionAfter;

VaultMetadataRequest(int maxVersions, boolean casRequired, String deleteVersionAfter) {
this.maxVersions = maxVersions;
this.casRequired = casRequired;
this.deleteVersionAfter = deleteVersionAfter;
}

public static VaultMetadataRequestBuilder builder() {
return new VaultMetadataRequestBuilder();
}

/**
* @return The number of versions to keep per key.
*/
public int getMaxVersions() {
return maxVersions;
}

/**
* @return If true all keys will require the cas parameter to be set on all write requests.
*/
public boolean isCasRequired() {
return casRequired;
}

/**
* @return the deletion_time for all new versions written to this key. Accepts <a href="https://golang.org/pkg/time/#ParseDuration">Go duration format string</a>.
*/
public String getDeleteVersionAfter() {
return deleteVersionAfter;
}

public static class VaultMetadataRequestBuilder {

private int maxVersions;
private boolean casRequired;
private String deleteVersionAfter;

/**
*
* sets the number of versions to keep per key.
*
* @param maxVersions
* @return {@link VaultMetadataRequest}
*/
public VaultMetadataRequestBuilder maxVersions(int maxVersions) {
this.maxVersions = maxVersions;
return this;
}

/**
*
* sets the cas_required parameter. If true all keys will require the cas parameter to be set on all write requests.
*
* @param casRequired
* @return {@link VaultMetadataRequest}
*/
public VaultMetadataRequestBuilder casRequired(boolean casRequired) {
this.casRequired = casRequired;
return this;
}

/**
* sets the deletion_time for all new versions written to this key. Accepts <a href="https://golang.org/pkg/time/#ParseDuration">Go duration format string</a>.
*
* @param deleteVersionAfter
* @return {@link VaultMetadataRequest}
*/
public VaultMetadataRequestBuilder deleteVersionAfter(String deleteVersionAfter) {
this.deleteVersionAfter = deleteVersionAfter;
return this;
}

/**
* @return a new {@link VaultMetadataRequest}
*/
public VaultMetadataRequest build() {
return new VaultMetadataRequest(maxVersions, casRequired, deleteVersionAfter);
}
}
}