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 ccaec7e2..f1c718d6 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 @@ -152,6 +152,11 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple VaultResponse response = doWrite(createDataPath(path), data); + if (response == null) { + throw new IllegalStateException( + "VaultVersionedKeyValueOperations cannot be used with a kv version 1 mount"); + } + return getMetadata(response.getRequiredData()); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java index b4ee423d..d9546e5e 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java @@ -50,13 +50,20 @@ public class VaultMount { */ private final Map config; + /** + * Mount type specific options. + */ + private final Map options; + VaultMount(@JsonProperty("type") String type, @Nullable @JsonProperty("description") String description, - @Nullable @JsonProperty("config") Map config) { + @Nullable @JsonProperty("config") Map config, + @Nullable @JsonProperty("options") Map options) { this.type = type; this.description = description; this.config = config != null ? config : Collections.emptyMap(); + this.options = options != null ? options : Collections.emptyMap(); } /** @@ -99,6 +106,16 @@ public class VaultMount { return config; } + /** + * @return mount type specific options. + * + * @since 2.2 + */ + @Nullable + public Map getOptions() { + return options; + } + /** * Builder to build a {@link VaultMount}. */ @@ -112,6 +129,8 @@ public class VaultMount { private Map config = Collections.emptyMap(); + private Map options = Collections.emptyMap(); + VaultMountBuilder() { } @@ -155,6 +174,22 @@ public class VaultMount { return this; } + /** + * Set mount type specific options for this mount. + * + * @param options mount type specific options for this mount. + * @return {@literal this} {@link VaultMountBuilder}. + * + * @since 2.2 + */ + public VaultMountBuilder options(Map options) { + + Assert.notNull(options, "Options map must not be null"); + + this.options = options; + return this; + } + /** * Build a new {@link VaultMount} instance. Requires {@link #type(String)} to be * configured. @@ -166,7 +201,7 @@ public class VaultMount { Assert.notNull(type, "Type must not be null"); Assert.hasText(type, "Type must not be empty or null"); - return new VaultMount(type, description, config); + return new VaultMount(type, description, config, options); } } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultSysTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultSysTemplateIntegrationTests.java index 7534d5ed..5a7165f0 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultSysTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultSysTemplateIntegrationTests.java @@ -30,12 +30,14 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.vault.support.Policy; import org.springframework.vault.support.VaultMount; +import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultUnsealStatus; import org.springframework.vault.support.Policy.Rule; import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.VaultRule; import org.springframework.vault.util.Version; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import static org.junit.Assume.assumeTrue; import static org.springframework.vault.support.Policy.BuiltinCapabilities.READ; import static org.springframework.vault.support.Policy.BuiltinCapabilities.UPDATE; @@ -82,8 +84,8 @@ public class VaultSysTemplateIntegrationTests extends IntegrationTestSupport { adminOperations.unmount("other"); } - VaultMount mount = VaultMount.builder().type("generic") // - .config(Collections.singletonMap("default_lease_ttl", (Object) "1h")) // + VaultMount mount = VaultMount.builder().type("generic") + .config(Collections.singletonMap("default_lease_ttl", "1h")) .description("hello, world").build(); adminOperations.mount("other", mount); @@ -98,6 +100,76 @@ public class VaultSysTemplateIntegrationTests extends IntegrationTestSupport { assertThat(Arrays.asList("kv", "generic")).contains(secret.getType()); } + @Test + public void mountShouldMountKv1Secret() { + + assumeTrue(vaultVersion.isGreaterThanOrEqualTo( + VaultRule.VERSIONING_INTRODUCED_WITH)); + + if (adminOperations.getMounts().containsKey("kVv1/")) { + adminOperations.unmount("kVv1"); + } + + VaultMount mount = VaultMount.builder().type("kv") + .config(Collections.singletonMap("default_lease_ttl", "1h")) + .description("hello, world").build(); + + adminOperations.mount("kVv1", mount); + + Map mounts = adminOperations.getMounts(); + + assertThat(mounts).containsKey("kVv1/"); + + VaultMount kVv1 = mounts.get("kVv1/"); + assertThat(kVv1.getDescription()).isEqualTo(mount.getDescription()); + assertThat(kVv1.getConfig()).containsEntry("default_lease_ttl", 3600); + assertThat(kVv1.getType()).isEqualTo("kv"); + + // a versioned write (kv put) will fail for a kv (default version: 1, not versioned) store + // make sure regular VaultTemplate.write/read operations work + + vaultOperations.write("secret/mykey", Collections.singletonMap("hello", "world")); + + VaultResponse read = vaultOperations.read("secret/mykey"); + assertThat(read).isNotNull(); + assertThat(read.getData()).containsEntry("hello", "world"); + } + + @Test + public void mountShouldMountKv2Secret() { + + assumeTrue(vaultVersion.isGreaterThanOrEqualTo( + VaultRule.VERSIONING_INTRODUCED_WITH)); + + if (adminOperations.getMounts().containsKey("kVv2/")) { + adminOperations.unmount("kVv2"); + } + + VaultMount mount = VaultMount.builder().type("kv") + .config(Collections.singletonMap("default_lease_ttl", "1h")) + .options(Collections.singletonMap("version", "2")) + .description("hello, world").build(); + + adminOperations.mount("kVv2", mount); + + Map mounts = adminOperations.getMounts(); + + assertThat(mounts).containsKey("kVv2/"); + + VaultMount kVv2 = mounts.get("kVv2/"); + assertThat(kVv2.getDescription()).isEqualTo(mount.getDescription()); + assertThat(kVv2.getConfig()).containsEntry("default_lease_ttl", 3600); + assertThat(kVv2.getType()).isEqualTo("kv"); + assertThat(kVv2.getOptions()).containsEntry("version", "2"); + + VaultVersionedKeyValueOperations versionedOperations = + vaultOperations.opsForVersionedKeyValue("kVv2"); + + versionedOperations.put("secret/mykey", Collections.singletonMap("key", "value")); + assertThat(versionedOperations.get("secret/mykey").getData()) + .containsEntry("key", "value"); + } + @Test public void getAuthMountsShouldContainSecretBackend() {