From c5a087acefa7e00f80df8e269ea6433715860ea7 Mon Sep 17 00:00:00 2001 From: Jeroen Willemsen Date: Mon, 14 Aug 2023 08:45:36 +0200 Subject: [PATCH] Add support for custom kv2 metadata. Original pull request: gh-808 Closes gh-789 --- .../core/VaultKeyValueMetadataTemplate.java | 14 ++++-- .../core/VaultVersionedKeyValueTemplate.java | 5 +++ .../vault/support/VaultMetadataRequest.java | 29 +++++++++++- .../vault/support/VaultMetadataResponse.java | 24 +++++++++- .../vault/support/Versioned.java | 44 ++++++++++++++++--- ...ionedKeyValueTemplateIntegrationTests.java | 26 +++++++++++ 6 files changed, 129 insertions(+), 13 deletions(-) diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java index cdef7e4a..9be44522 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java @@ -98,6 +98,7 @@ class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations { .oldestVersion(Integer.parseInt(String.valueOf(metadataResponse.get("oldest_version")))) .updatedTime(toInstant((String) metadataResponse.get("updated_time"))) .versions(buildVersions((Map) metadataResponse.get("versions"))) + .customMetadata((Map) metadataResponse.get("custom_metadata")) .build(); } @@ -115,13 +116,18 @@ class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations { Instant deletionTime = toInstant((String) versionData.get("deletion_time")); boolean destroyed = (Boolean) versionData.get("destroyed"); Versioned.Version kvVersion = Versioned.Version.from(Integer.parseInt(version)); - - return Versioned.Metadata.builder() + Versioned.Metadata.MetadataBuilder builder = Versioned.Metadata.builder() .createdAt(createdTime) .deletedAt(deletionTime) .destroyed(destroyed) - .version(kvVersion) - .build(); + .version(kvVersion); + + if (versionData.get("custom_metadata") != null) { + Map customMetadata = (Map) versionData.get("custom_metadata"); + builder.customMetadata(customMetadata); + } + + return builder.build(); } @Nullable diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java index 3e429b57..dde6be23 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java @@ -178,6 +178,11 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple Integer version = (Integer) responseMetadata.get("version"); builder.version(Version.from(version)); + if (responseMetadata.get("custom_metadata") != null) { + Map customMetadata = (Map) responseMetadata.get("custom_metadata"); + builder.customMetadata(customMetadata); + } + return builder.build(); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataRequest.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataRequest.java index 471ea6c6..88a5dcec 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataRequest.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataRequest.java @@ -16,6 +16,7 @@ package org.springframework.vault.support; import java.time.Duration; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonProperty; @@ -41,11 +42,16 @@ public class VaultMetadataRequest { @JsonProperty("delete_version_after") private final String deleteVersionAfter; - private VaultMetadataRequest(int maxVersions, boolean casRequired, @Nullable Duration deleteVersionAfter) { + @JsonProperty("custom_metadata") + private final Map customMetadata; + + private VaultMetadataRequest(int maxVersions, boolean casRequired, @Nullable Duration deleteVersionAfter, + @Nullable Map customMetadata) { this.maxVersions = maxVersions; this.casRequired = casRequired; this.deleteVersionAfter = DurationParser .formatDuration(deleteVersionAfter != null ? deleteVersionAfter : Duration.ZERO); + this.customMetadata = customMetadata; } public static VaultMetadataRequestBuilder builder() { @@ -75,6 +81,11 @@ public class VaultMetadataRequest { return this.deleteVersionAfter; } + @Nullable + public Map getCustomMetadata() { + return this.customMetadata; + } + public static class VaultMetadataRequestBuilder { private int maxVersions; @@ -84,6 +95,9 @@ public class VaultMetadataRequest { @Nullable private Duration deleteVersionAfter; + @Nullable + private Map customMetadata; + /** * Set the number of versions to keep per key. * @param maxVersions @@ -115,11 +129,22 @@ public class VaultMetadataRequest { return this; } + /** + * Sets the custom Metadata for the metadatarequest + * @param customMetadata + * @return {@link VaultMetadataRequest} + */ + public VaultMetadataRequestBuilder customMetadata(Map customMetadata) { + this.customMetadata = customMetadata; + return this; + } + /** * @return a new {@link VaultMetadataRequest} */ public VaultMetadataRequest build() { - return new VaultMetadataRequest(this.maxVersions, this.casRequired, this.deleteVersionAfter); + return new VaultMetadataRequest(this.maxVersions, this.casRequired, this.deleteVersionAfter, + this.customMetadata); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataResponse.java index 41702cae..1b8aafa1 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataResponse.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMetadataResponse.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.time.Instant; import java.time.Period; import java.util.List; +import java.util.Map; import org.springframework.lang.Nullable; @@ -46,9 +47,11 @@ public class VaultMetadataResponse { private final List versions; + private final Map customMetadata; + private VaultMetadataResponse(boolean casRequired, Instant createdTime, int currentVersion, Duration deleteVersionAfter, int maxVersions, int oldestVersion, Instant updatedTime, - List versions) { + List versions, Map customMetadata) { this.casRequired = casRequired; this.createdTime = createdTime; this.currentVersion = currentVersion; @@ -57,6 +60,7 @@ public class VaultMetadataResponse { this.oldestVersion = oldestVersion; this.updatedTime = updatedTime; this.versions = versions; + this.customMetadata = customMetadata; } public static VaultMetadataResponseBuilder builder() { @@ -93,6 +97,14 @@ public class VaultMetadataResponse { return this.deleteVersionAfter; } + /** + * @return KV of customMetadata. Entries can be any arbitrary key-value pairs + */ + @Nullable + public Map getCustomMetadata() { + return this.customMetadata; + } + /** * @return max secret versions accepted by this key */ @@ -145,6 +157,8 @@ public class VaultMetadataResponse { private List versions; + private Map customMetadata; + public VaultMetadataResponseBuilder casRequired(boolean casRequired) { this.casRequired = casRequired; return this; @@ -185,9 +199,15 @@ public class VaultMetadataResponse { return this; } + public VaultMetadataResponseBuilder customMetadata(Map customMetadata) { + this.customMetadata = customMetadata; + return this; + } + public VaultMetadataResponse build() { return new VaultMetadataResponse(this.casRequired, this.createdTime, this.currentVersion, - this.deleteVersionAfter, this.maxVersions, this.oldestVersion, this.updatedTime, this.versions); + this.deleteVersionAfter, this.maxVersions, this.oldestVersion, this.updatedTime, this.versions, + this.customMetadata); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/Versioned.java b/spring-vault-core/src/main/java/org/springframework/vault/support/Versioned.java index 2479768c..950a7ac2 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/Versioned.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/Versioned.java @@ -16,6 +16,7 @@ package org.springframework.vault.support; import java.time.Instant; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -32,7 +33,7 @@ import org.springframework.util.Assert; *
  • Versioned secrets with {@link Metadata} attached * {@link Versioned#create(Object, Metadata)}
  • * - * + *

    * Versioned secrets follow a lifecycle that spans from creation to destruction: * *

      @@ -44,9 +45,9 @@ import org.springframework.util.Assert; *
    * * @author Mark Paluch - * @since 2.1 * @see Version * @see Metadata + * @since 2.1 */ public class Versioned { @@ -223,11 +224,15 @@ public class Versioned { private final Version version; - private Metadata(Instant createdAt, @Nullable Instant deletedAt, boolean destroyed, Version version) { + private final @Nullable Map customMetadata; + + private Metadata(Instant createdAt, @Nullable Instant deletedAt, boolean destroyed, Version version, + @Nullable Map customMetadata) { this.createdAt = createdAt; this.deletedAt = deletedAt; this.destroyed = destroyed; this.version = version; + this.customMetadata = customMetadata; } /** @@ -275,11 +280,29 @@ public class Versioned { return this.destroyed; } + /** + * @return Metadata . + */ + @Nullable + public Map getCustomMetadata() { + return customMetadata; + } + @Override public String toString() { + String customMetadataString = ""; + if (customMetadata != null && customMetadata.keySet().size() > 0) { + StringBuilder metadataPrintBuilder = new StringBuilder(", customMetadata=Map["); + for (String key : customMetadata.keySet()) { + metadataPrintBuilder.append(key).append(":").append(customMetadata.get(key)).append(" "); + } + metadataPrintBuilder.append("]"); + customMetadataString = metadataPrintBuilder.toString(); + } + return getClass().getSimpleName() + " [createdAt=" + this.createdAt + ", deletedAt=" + this.deletedAt - + ", destroyed=" + this.destroyed + ", version=" + this.version + ']'; + + ", destroyed=" + this.destroyed + ", version=" + this.version + customMetadataString + ']'; } /** @@ -295,6 +318,8 @@ public class Versioned { private @Nullable Version version; + private @Nullable Map customMetadata; + private MetadataBuilder() { } @@ -354,6 +379,15 @@ public class Versioned { return this; } + public MetadataBuilder customMetadata(Map customMetadata) { + + Assert.notNull(customMetadata, "customMetadata should not be null"); + Assert.notEmpty(customMetadata.keySet(), "customMetadata should have at least one key"); + Assert.notEmpty(customMetadata.values(), "customMetadata should have at least one value"); + this.customMetadata = customMetadata; + return this; + } + /** * Build the {@link Versioned.Metadata} object. Requires * {@link #createdAt(Instant)} and {@link #version(Versioned.Version)} to be @@ -365,7 +399,7 @@ public class Versioned { Assert.notNull(this.createdAt, "CreatedAt must not be null"); Assert.notNull(this.version, "Version must not be null"); - return new Metadata(this.createdAt, this.deletedAt, this.destroyed, this.version); + return new Metadata(this.createdAt, this.deletedAt, this.destroyed, this.version, this.customMetadata); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java index 9d9bf4f9..1e49cd24 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.vault.core; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -29,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.vault.VaultException; import org.springframework.vault.domain.Person; +import org.springframework.vault.support.VaultMetadataRequest; import org.springframework.vault.support.Versioned; import org.springframework.vault.support.Versioned.Metadata; import org.springframework.vault.support.Versioned.Version; @@ -103,6 +105,30 @@ class VaultVersionedKeyValueTemplateIntegrationTests extends IntegrationTestSupp .hasMessageContaining("check-and-set parameter did not match the current version"); } + @Test + void shouldWriteSecretWithCustomMetadata() { + Person person = new Person(); + person.setFirstname("Walter"); + person.setLastname("White"); + + String key = UUID.randomUUID().toString(); + + Map customMetadata = new HashMap<>(); + customMetadata.put("foo", "bar"); + customMetadata.put("uid", "werwer"); + + this.versionedOperations.put(key, Versioned.create(person)); + + VaultMetadataRequest request = VaultMetadataRequest.builder().customMetadata(customMetadata).build(); + + this.versionedOperations.opsForKeyValueMetadata().put(key, request); + + Versioned versioned = this.versionedOperations.get(key, Person.class); + + assertThat(versioned.getMetadata().getCustomMetadata().get("foo")).isEqualTo("bar"); + + } + @Test void shouldReadAndWriteVersionedSecret() {