From 6413a20c7ea600daf4f23f6a863722806dd68bd9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 28 May 2020 09:12:41 +0200 Subject: [PATCH] Polishing Reformat sources from space indents to tabs. Introduce DurationParser to represent java.time.Duration using Go's Duration format. Reduce visibility of implementation to package level. Simplify code. Update tests to work with rerunning tests. Reorder methods. Add license headers. Original pull request: gh-561. Resolves gh-432. --- .../core/VaultKeyValueMetadataOperations.java | 61 ++-- .../core/VaultKeyValueMetadataTemplate.java | 184 +++++++----- .../vault/support/DurationParser.java | 134 +++++++++ .../vault/support/VaultMetadataRequest.java | 181 ++++++----- .../vault/support/VaultMetadataResponse.java | 284 +++++++++--------- ...ValueMetadataTemplateIntegrationTests.java | 176 +++++++---- ...sionedKeyValueBackendIntegrationTests.java | 1 + .../support/DurationParserUnitTests.java | 62 ++++ src/main/asciidoc/new-features.adoc | 1 + 9 files changed, 710 insertions(+), 374 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/support/DurationParser.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/support/DurationParserUnitTests.java diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataOperations.java index 195c5558..2cc389b4 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataOperations.java @@ -1,34 +1,55 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.vault.core; +import org.springframework.lang.Nullable; import org.springframework.vault.support.VaultMetadataRequest; import org.springframework.vault.support.VaultMetadataResponse; /** - * Interface that specifies kv metadata related operations + * Interface that specifies kv metadata related operations. * * @author Zakaria Amine - * @see kv backend metadata api docs + * @see Key-Value + * Metadata API + * @since 2.3 */ 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); + /** + * Retrieve the metadata and versions for the secret at the specified path. + * @param path the secret path, must not be {@literal null} or empty. + * @return {@link VaultMetadataResponse} + */ + @Nullable + VaultMetadataResponse get(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); + /** + * Update the secret metadata, or creates new metadata if not present. + * + * @param path the secret path, must not be {@literal null} or empty. + * @param body {@link VaultMetadataRequest} + */ + void put(String path, VaultMetadataRequest body); - /** - * 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); + /** + * Permanently delete 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 {@literal null} or empty. + */ + void delete(String path); } 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 9028e865..baa90176 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 @@ -1,107 +1,135 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.vault.core; +import java.time.Duration; 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.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.vault.client.VaultResponses; +import org.springframework.util.StringUtils; +import org.springframework.vault.support.DurationParser; import org.springframework.vault.support.VaultMetadataRequest; import org.springframework.vault.support.VaultMetadataResponse; +import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.support.Versioned; -import org.springframework.web.client.HttpStatusCodeException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +/** + * Default implementation of {@link VaultKeyValueMetadataOperations}. + * + * @author Zakaria Amine + * @author Mark Paluch + * @since 2.3 + */ +class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations { -public class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations { + private final VaultOperations vaultOperations; - private final VaultOperations vaultOperations; + private final String basePath; - private final String basePath; + VaultKeyValueMetadataTemplate(VaultOperations vaultOperations, String basePath) { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + Assert.notNull(vaultOperations, "VaultOperations must not be null"); - public VaultKeyValueMetadataTemplate(VaultOperations vaultOperations, String basePath) { - Assert.notNull(vaultOperations, "VaultOperations must not be null"); - this.vaultOperations = vaultOperations; - this.basePath = basePath; - } + 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 + @SuppressWarnings({ "rawtypes", "unchecked" }) + public VaultMetadataResponse get(String path) { - @Override - public VaultMetadataResponse get(String path) { - Assert.hasText(path, "Path must not be empty"); - Map metadataResponse = - vaultOperations.read("/" + this.basePath + "/metadata/" + path, Map.class).getData(); + VaultResponseSupport response = this.vaultOperations.read(getPath(path), + Map.class); - return fromMap(metadataResponse); - } + return response != null ? fromMap(response.getRequiredData()) : null; + } - @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); - } - }); - } + @Override + public void put(String path, VaultMetadataRequest body) { - private VaultMetadataResponse fromMap(Map 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(); - } + Assert.hasText(path, "Path must not be empty"); + Assert.notNull(body, "Body must not be null"); - private static List buildVersions(Object versions) { - try { - JsonNode kvVersions = OBJECT_MAPPER.readTree(OBJECT_MAPPER.writeValueAsString(versions)); + this.vaultOperations.write(getPath(path), body); + } - 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<>(); - } - } + @Override + public void delete(String path) { - 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)); + Assert.hasText(path, "Path must not be empty"); - return Versioned.Metadata.builder().createdAt(createdTime).deletedAt(deletionTime).destroyed(destroyed).version(kvVersion).build(); - } + this.vaultOperations.delete(getPath(path)); + } - private static Instant toInstant(Object date) { - return Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(String.valueOf(date))); - } + private String getPath(String path) { + + Assert.hasText(path, "Path must not be empty"); + return this.basePath + "/metadata/" + path; + } + + private static VaultMetadataResponse fromMap(Map metadataResponse) { + + Duration duration = DurationParser + .parseDuration((String) metadataResponse.get("delete_version_after")); + + return VaultMetadataResponse.builder() + .casRequired(Boolean.parseBoolean( + String.valueOf(metadataResponse.get("cas_required")))) + .createdTime(toInstant((String) metadataResponse.get("created_time"))) + .currentVersion(Integer.parseInt( + String.valueOf(metadataResponse.get("current_version")))) + .deleteVersionAfter(duration) + .maxVersions(Integer + .parseInt(String.valueOf(metadataResponse.get("max_versions")))) + .oldestVersion(Integer + .parseInt(String.valueOf(metadataResponse.get("oldest_version")))) + .updatedTime(toInstant((String) metadataResponse.get("updated_time"))) + .versions(buildVersions((Map) metadataResponse.get("versions"))).build(); + } + + private static List buildVersions( + Map> versions) { + + return versions.entrySet().stream() + .map(entry -> buildVersion(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private static Versioned.Metadata buildVersion(String version, + Map versionData) { + + Instant createdTime = toInstant((String) versionData.get("created_time")); + 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().createdAt(createdTime).deletedAt(deletionTime) + .destroyed(destroyed).version(kvVersion).build(); + } + + @Nullable + private static Instant toInstant(String date) { + + return StringUtils.hasText(date) + ? Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date)) + : null; + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/DurationParser.java b/spring-vault-core/src/main/java/org/springframework/vault/support/DurationParser.java new file mode 100644 index 00000000..e41c64c9 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/DurationParser.java @@ -0,0 +1,134 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Utility to parse a Go format duration into {@link Duration}. + * + * @author Mark Paluch + * @since 2.3 + * @see Go ParseDuration + */ +public class DurationParser { + + private static final Pattern PARSE_PATTERN = Pattern + .compile("([0-9]+)(ns|us|ms|s|m|h|d)"); + private static final Pattern VERIFY_PATTERN = Pattern + .compile("(([0-9]+)(ns|us|ms|s|m|h|d))+"); + + /** + * Parse a Go format duration into a {@link Duration} object. + * + * @param duration the duration string to parse in Go's duration format. + * @return the duration object. Can be {@literal null} if {@code duration} is empty. + * @throws IllegalArgumentException if unable to parse the requested duration. + */ + @Nullable + public static Duration parseDuration(String duration) { + + if (StringUtils.isEmpty(duration)) { + return null; + } + + if ("0".equals(duration)) { + return Duration.ZERO; + } + + if (!VERIFY_PATTERN.matcher(duration.toLowerCase(Locale.ENGLISH)).matches()) { + throw new IllegalArgumentException( + String.format("Cannot parse '%s' into a Duration", duration)); + } + + Matcher matcher = PARSE_PATTERN.matcher(duration.toLowerCase(Locale.ENGLISH)); + Duration result = Duration.ZERO; + while (matcher.find()) { + + int num = Integer.parseInt(matcher.group(1)); + String typ = matcher.group(2); + + switch (typ) { + case "ns": + result = result.plus(Duration.ofNanos(num)); + break; + case "us": + result = result.plus(Duration.ofNanos(num * 1000)); + break; + case "ms": + result = result.plus(Duration.ofMillis(num)); + break; + case "s": + result = result.plus(Duration.ofSeconds(num)); + break; + case "m": + result = result.plus(Duration.ofMinutes(num)); + break; + case "h": + result = result.plus(Duration.ofHours(num)); + break; + case "d": + result = result.plus(Duration.ofDays(num)); + break; + case "w": + result = result.plus(Duration.ofDays(num * 7)); + break; + } + } + + return result; + } + + /** + * Format a {@link Duration} into the Go format representation. + * + * @param duration the duration object to format. + * @return the duration formatted in Go's duration format. + */ + public static String formatDuration(Duration duration) { + + StringBuilder builder = new StringBuilder(); + + for (TemporalUnit unit : duration.getUnits()) { + + if (unit == ChronoUnit.MINUTES) { + builder.append(duration.get(unit)).append('m'); + } + if (unit == ChronoUnit.HOURS) { + builder.append(duration.get(unit)).append('h'); + } + if (unit == ChronoUnit.SECONDS) { + builder.append(duration.get(unit)).append('s'); + } + if (unit == ChronoUnit.MILLIS) { + builder.append(duration.get(unit)).append("ms"); + } + if (unit == ChronoUnit.NANOS) { + builder.append(duration.get(unit)).append("ns"); + } + } + + return builder.toString(); + } +} 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 da1d8b5d..f7e4fede 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 @@ -1,101 +1,130 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.vault.support; +import java.time.Duration; + import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.lang.Nullable; + /** * Value object to bind Vault HTTP kv metadata update API requests. * * @author Zakaria Amine - * @see Update Metadata + * @see Update + * Metadata + * @since 2.3 */ public class VaultMetadataRequest { - @JsonProperty("max_versions") - private int maxVersions; + @JsonProperty("max_versions") + private final int maxVersions; - @JsonProperty("cas_required") - private boolean casRequired; + @JsonProperty("cas_required") + private final boolean casRequired; - @JsonProperty("delete_version_after") - private String deleteVersionAfter; + @JsonProperty("delete_version_after") + private final String deleteVersionAfter; - VaultMetadataRequest(int maxVersions, boolean casRequired, String deleteVersionAfter) { - this.maxVersions = maxVersions; - this.casRequired = casRequired; - this.deleteVersionAfter = deleteVersionAfter; - } + private VaultMetadataRequest(int maxVersions, boolean casRequired, + @Nullable Duration deleteVersionAfter) { + this.maxVersions = maxVersions; + this.casRequired = casRequired; + this.deleteVersionAfter = DurationParser.formatDuration( + deleteVersionAfter != null ? deleteVersionAfter : Duration.ZERO); + } - public static VaultMetadataRequestBuilder builder() { - return new VaultMetadataRequestBuilder(); - } + public static VaultMetadataRequestBuilder builder() { + return new VaultMetadataRequestBuilder(); + } - /** - * @return The number of versions to keep per key. - */ - public int getMaxVersions() { - return maxVersions; - } + /** + * @return The number of versions to keep per key. + */ + public int getMaxVersions() { + return this.maxVersions; + } - /** - * @return If true all keys will require the cas parameter to be set on all write requests. - */ - public boolean isCasRequired() { - return casRequired; - } + /** + * @return If true all keys will require the cas parameter to be set on all write + * requests. + */ + public boolean isCasRequired() { + return this.casRequired; + } - /** - * @return the deletion_time for all new versions written to this key. Accepts Go duration format string. - */ - public String getDeleteVersionAfter() { - return deleteVersionAfter; - } + /** + * @return the deletion_time for all new versions written to this key. Accepts + * Go duration format string. + */ + public String getDeleteVersionAfter() { + return this.deleteVersionAfter; + } - public static class VaultMetadataRequestBuilder { + public static class VaultMetadataRequestBuilder { - private int maxVersions; - private boolean casRequired; - private String deleteVersionAfter; + private int maxVersions; + private boolean casRequired; - /** - * - * sets the number of versions to keep per key. - * - * @param maxVersions - * @return {@link VaultMetadataRequest} - */ - public VaultMetadataRequestBuilder maxVersions(int maxVersions) { - this.maxVersions = maxVersions; - return this; - } + @Nullable + private Duration deleteVersionAfter; - /** - * - * 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; - } + /** + * Set 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 deletion_time for all new versions written to this key. Accepts Go duration format string. - * - * @param deleteVersionAfter - * @return {@link VaultMetadataRequest} - */ - public VaultMetadataRequestBuilder deleteVersionAfter(String deleteVersionAfter) { - this.deleteVersionAfter = deleteVersionAfter; - return this; - } + /** + * Set 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; + } - /** - * @return a new {@link VaultMetadataRequest} - */ - public VaultMetadataRequest build() { - return new VaultMetadataRequest(maxVersions, casRequired, deleteVersionAfter); - } - } + /** + * Sets the deletion time for all new versions written to this key. + * + * @param deleteVersionAfter + * @return {@link VaultMetadataRequest} + */ + public VaultMetadataRequestBuilder deleteVersionAfter( + Duration deleteVersionAfter) { + this.deleteVersionAfter = deleteVersionAfter; + return this; + } + + /** + * @return a new {@link VaultMetadataRequest} + */ + public VaultMetadataRequest build() { + return new VaultMetadataRequest(this.maxVersions, this.casRequired, + this.deleteVersionAfter); + } + } } 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 5d80c233..c18caedb 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 @@ -1,180 +1,186 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.vault.support; +import java.time.Duration; import java.time.Instant; +import java.time.Period; import java.util.List; -import java.util.Map; /** * Value object to bind Vault HTTP kv read metadata API responses. * * @author Zakaria Amine + * @since 2.3 */ public class VaultMetadataResponse { - private boolean casRequired; + private final boolean casRequired; - private Instant createdTime; + private final Instant createdTime; - private int currentVersion; + private final int currentVersion; - private String deleteVersionAfter; + private final Duration deleteVersionAfter; - private int maxVersions; + private final int maxVersions; - private int oldestVersion; + private final int oldestVersion; - private Instant updatedTime; + private final Instant updatedTime; - private List versions; + private final List versions; - VaultMetadataResponse(boolean casRequired, Instant createdTime, int currentVersion, String deleteVersionAfter, - int maxVersions, int oldestVersion, Instant updatedTime, List versions) { - this.casRequired = casRequired; - this.createdTime = createdTime; - this.currentVersion = currentVersion; - this.deleteVersionAfter = deleteVersionAfter; - this.maxVersions = maxVersions; - this.oldestVersion = oldestVersion; - this.updatedTime = updatedTime; - this.versions = versions; - } + private VaultMetadataResponse(boolean casRequired, Instant createdTime, + int currentVersion, Duration deleteVersionAfter, int maxVersions, + int oldestVersion, Instant updatedTime, List versions) { + this.casRequired = casRequired; + this.createdTime = createdTime; + this.currentVersion = currentVersion; + this.deleteVersionAfter = deleteVersionAfter; + this.maxVersions = maxVersions; + this.oldestVersion = oldestVersion; + this.updatedTime = updatedTime; + this.versions = versions; + } - public static VaultMetadataResponseBuilder builder() {return new VaultMetadataResponseBuilder();} + public static VaultMetadataResponseBuilder builder() { + return new VaultMetadataResponseBuilder(); + } - /** - * - * @return - */ - public boolean isCasRequired() { - return casRequired; - } + /** + * @return whether compare-and-swap is required (i.e. optimistic locking). + */ + public boolean isCasRequired() { + return this.casRequired; + } - /** - * - * @return the metadata creation time - */ - public Instant getCreatedTime() { - return createdTime; - } + /** + * @return the metadata creation time + */ + public Instant getCreatedTime() { + return this.createdTime; + } - /** - * - * @return the active secret version - */ - public int getCurrentVersion() { - return currentVersion; - } + /** + * @return the active secret version + */ + public int getCurrentVersion() { + return this.currentVersion; + } - /** - * - * @return the duration after which a secret is to be deleted. 0 for unlimited duration. follows Go duration format string. - */ - public String getDeleteVersionAfter() { - return deleteVersionAfter; - } + /** + * @return the duration after which a secret is to be deleted. {@link Period#ZERO} for + * unlimited duration. + */ + public Duration getDeleteVersionAfter() { + return this.deleteVersionAfter; + } - /** - * - * @return max secret versions accepted by this key - */ - public int getMaxVersions() { - return maxVersions; - } + /** + * @return max secret versions accepted by this key + */ + public int getMaxVersions() { + return this.maxVersions; + } - /** - * - * @return oldest key version - */ - public int getOldestVersion() { - return oldestVersion; - } + /** + * @return oldest key version + */ + public int getOldestVersion() { + return this.oldestVersion; + } - /** - * - * @return the metadata update time - */ - public Instant getUpdatedTime() { - return updatedTime; - } + /** + * @return the metadata update time + */ + public Instant getUpdatedTime() { + return this.updatedTime; + } - /** - * - * Follows the following format. - * - * "versions": { - * "1": { - * "created_time": "2020-05-18T12:23:09.895587932Z", - * "deletion_time": "2020-05-18T12:31:00.66257744Z", - * "destroyed": false - * }, - * "2": { - * "created_time": "2020-05-18T12:23:10.122081788Z", - * "deletion_time": "", - * "destroyed": false - * } - * } - * - * @return the key versions and their details - */ - public List getVersions() { - return versions; - } + /** + * Follows the following format. + * + * "versions": { "1": { "created_time": "2020-05-18T12:23:09.895587932Z", + * "deletion_time": "2020-05-18T12:31:00.66257744Z", "destroyed": false }, "2": { + * "created_time": "2020-05-18T12:23:10.122081788Z", "deletion_time": "", "destroyed": + * false } } + * + * @return the key versions and their details + */ + public List getVersions() { + return this.versions; + } + public static class VaultMetadataResponseBuilder { - public static class VaultMetadataResponseBuilder { + private boolean casRequired; + private Instant createdTime; + private int currentVersion; + private Duration deleteVersionAfter; + private int maxVersions; + private int oldestVersion; + private Instant updatedTime; + private List versions; - private boolean casRequired; - private Instant createdTime; - private int currentVersion; - private String deleteVersionAfter; - private int maxVersions; - private int oldestVersion; - private Instant updatedTime; - private List versions; + public VaultMetadataResponseBuilder casRequired(boolean casRequired) { + this.casRequired = casRequired; + return this; + } - public VaultMetadataResponseBuilder casRequired(boolean casRequired) { - this.casRequired = casRequired; - return this; - } + public VaultMetadataResponseBuilder createdTime(Instant createdTime) { + this.createdTime = createdTime; + return this; + } - public VaultMetadataResponseBuilder createdTime(Instant createdTime) { - this.createdTime = createdTime; - return this; - } + public VaultMetadataResponseBuilder currentVersion(int currentVersion) { + this.currentVersion = currentVersion; + return this; + } - public VaultMetadataResponseBuilder currentVersion(int currentVersion) { - this.currentVersion = currentVersion; - return this; - } + public VaultMetadataResponseBuilder deleteVersionAfter( + Duration deleteVersionAfter) { + this.deleteVersionAfter = deleteVersionAfter; + return this; + } - public VaultMetadataResponseBuilder deleteVersionAfter(String deleteVersionAfter) { - this.deleteVersionAfter = deleteVersionAfter; - return this; - } + public VaultMetadataResponseBuilder maxVersions(int maxVersions) { + this.maxVersions = maxVersions; + return this; + } - public VaultMetadataResponseBuilder maxVersions(int maxVersions) { - this.maxVersions = maxVersions; - return this; - } + public VaultMetadataResponseBuilder oldestVersion(int oldestVersion) { + this.oldestVersion = oldestVersion; + return this; + } - public VaultMetadataResponseBuilder oldestVersion(int oldestVersion) { - this.oldestVersion = oldestVersion; - return this; - } + public VaultMetadataResponseBuilder updatedTime(Instant updatedTime) { + this.updatedTime = updatedTime; + return this; + } - public VaultMetadataResponseBuilder updatedTime(Instant updatedTime) { - this.updatedTime = updatedTime; - return this; - } + public VaultMetadataResponseBuilder versions(List versions) { + this.versions = versions; + return this; + } - public VaultMetadataResponseBuilder versions(List versions) { - this.versions = versions; - return this; - } - - public VaultMetadataResponse build() { - return new VaultMetadataResponse(casRequired, createdTime, currentVersion, deleteVersionAfter, maxVersions, - oldestVersion, updatedTime, versions); - } - } + public VaultMetadataResponse build() { + return new VaultMetadataResponse(this.casRequired, this.createdTime, + this.currentVersion, this.deleteVersionAfter, this.maxVersions, + this.oldestVersion, this.updatedTime, this.versions); + } + } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultKeyValueMetadataTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultKeyValueMetadataTemplateIntegrationTests.java index 49b15f79..bee192be 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultKeyValueMetadataTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultKeyValueMetadataTemplateIntegrationTests.java @@ -1,13 +1,29 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.vault.core; +import java.time.Duration; import java.time.Instant; import java.util.HashMap; import java.util.Map; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.vault.support.VaultMetadataRequest; @@ -17,87 +33,125 @@ import org.springframework.vault.support.Versioned; import static org.assertj.core.api.Assertions.assertThat; +/** + * Integration tests for {@link VaultKeyValueMetadataOperations}. + * + * @author Zakaria Amine + * @author Mark Paluch + */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = VaultIntegrationTestConfiguration.class) -public class VaultKeyValueMetadataTemplateIntegrationTests extends AbstractVaultKeyValueTemplateIntegrationTests { +class VaultKeyValueMetadataTemplateIntegrationTests + extends AbstractVaultKeyValueTemplateIntegrationTests { - private static final String SECRET_NAME = "test"; - private VaultKeyValueMetadataOperations vaultKeyValueMetadataOperations; + private static final String SECRET_NAME = "regular-test"; - VaultKeyValueMetadataTemplateIntegrationTests() { - super("versioned", VaultKeyValueOperationsSupport.KeyValueBackend.versioned()); - } + private static final String CAS_SECRET_NAME = "cas-test"; - @BeforeEach - void setup() { - Map secret = new HashMap<>(); - secret.put("key", "value"); + private VaultKeyValueMetadataOperations vaultKeyValueMetadataOperations; - kvOperations.put(SECRET_NAME, secret); - vaultKeyValueMetadataOperations = vaultOperations.opsForVersionedKeyValue("versioned").opsForKeyValueMetadata(); - } + VaultKeyValueMetadataTemplateIntegrationTests() { + super("versioned", VaultKeyValueOperationsSupport.KeyValueBackend.versioned()); + } - @Test - public void shouldReadMetadataForANewKVEntry() { + @BeforeEach + void setup() { - VaultMetadataResponse metadataResponse = vaultKeyValueMetadataOperations.get(SECRET_NAME); + vaultKeyValueMetadataOperations = vaultOperations + .opsForVersionedKeyValue("versioned").opsForKeyValueMetadata(); - assertThat(metadataResponse.getMaxVersions()).isEqualTo(0); - assertThat(metadataResponse.getCurrentVersion()).isEqualTo(1); - assertThat(metadataResponse.getVersions()).hasSize(1); - assertThat(metadataResponse.isCasRequired()).isFalse(); - assertThat(metadataResponse.getDeleteVersionAfter()).isEqualTo("0s"); - assertThat(metadataResponse.getCreatedTime().isBefore(Instant.now())).isTrue(); - assertThat(metadataResponse.getUpdatedTime().isBefore(Instant.now())).isTrue(); + try { + vaultKeyValueMetadataOperations.delete(SECRET_NAME); + } + catch (Exception e) { + // ignore + } - Versioned.Metadata version1 = metadataResponse.getVersions().get(0); + try { + vaultKeyValueMetadataOperations.delete(CAS_SECRET_NAME); + } + catch (Exception e) { + // ignore + } - assertThat(version1.getDeletedAt()).isNull(); - assertThat(version1.getCreatedAt()).isBefore(Instant.now()); - assertThat(version1.getVersion().getVersion()).isEqualTo(1); - } + Map secret = new HashMap<>(); + secret.put("key", "value"); - @Test - public void shouldUpdateMetadataVersions() { - Map secret = new HashMap<>(); - secret.put("newkey", "newvalue"); - kvOperations.put(SECRET_NAME, secret); + kvOperations.put(SECRET_NAME, secret); + } - VaultMetadataResponse metadataResponse = vaultKeyValueMetadataOperations.get(SECRET_NAME); + @Test + void shouldReadMetadataForANewKVEntry() { - assertThat(metadataResponse.getCurrentVersion()).isEqualTo(2); - assertThat(metadataResponse.getVersions()).hasSize(2); - } + VaultMetadataResponse metadataResponse = vaultKeyValueMetadataOperations + .get(SECRET_NAME); - @Test - public void shouldUpdateKVMetadata() { - VaultMetadataRequest request = VaultMetadataRequest.builder().casRequired(true).deleteVersionAfter("6h30m0s").maxVersions(20).build(); + assertThat(metadataResponse.getMaxVersions()).isEqualTo(0); + assertThat(metadataResponse.getCurrentVersion()).isEqualTo(1); + assertThat(metadataResponse.getVersions()).hasSize(1); + assertThat(metadataResponse.isCasRequired()).isFalse(); + assertThat(metadataResponse.getDeleteVersionAfter()).isEqualTo(Duration.ZERO); + assertThat(metadataResponse.getCreatedTime().isBefore(Instant.now())).isTrue(); + assertThat(metadataResponse.getUpdatedTime().isBefore(Instant.now())).isTrue(); - vaultKeyValueMetadataOperations.put(SECRET_NAME, request); + Versioned.Metadata version1 = metadataResponse.getVersions().get(0); - VaultMetadataResponse metadataResponseAfterUpdate = vaultKeyValueMetadataOperations.get(SECRET_NAME); + assertThat(version1.getDeletedAt()).isNull(); + assertThat(version1.getCreatedAt()).isBefore(Instant.now()); + assertThat(version1.getVersion().getVersion()).isEqualTo(1); + } - assertThat(metadataResponseAfterUpdate.isCasRequired()).isEqualTo(request.isCasRequired()); - assertThat(metadataResponseAfterUpdate.getMaxVersions()).isEqualTo(request.getMaxVersions()); - assertThat(metadataResponseAfterUpdate.getDeleteVersionAfter()).isEqualTo(request.getDeleteVersionAfter()); - } + @Test + void shouldUpdateMetadataVersions() { - @Test - public void shouldDeleteMetadata() { - kvOperations.delete(SECRET_NAME); - VaultMetadataResponse metadataResponse = vaultKeyValueMetadataOperations.get(SECRET_NAME); - Versioned.Metadata version1 = metadataResponse.getVersions().get(0); - assertThat(version1.getDeletedAt()).isBefore(Instant.now()); + Map secret = new HashMap<>(); + secret.put("newkey", "newvalue"); + kvOperations.put(SECRET_NAME, secret); - vaultKeyValueMetadataOperations.delete(SECRET_NAME); + VaultMetadataResponse metadataResponse = vaultKeyValueMetadataOperations + .get(SECRET_NAME); - VaultResponse response = kvOperations.get(SECRET_NAME); + assertThat(metadataResponse.getCurrentVersion()).isEqualTo(2); + assertThat(metadataResponse.getVersions()).hasSize(2); + } - assertThat(response).isNull(); - } + @Test + void shouldUpdateKVMetadata() { - @AfterEach - void cleanup() { - vaultKeyValueMetadataOperations.delete(SECRET_NAME); - } + Map secret = new HashMap<>(); + secret.put("key", "value"); + + kvOperations.put(CAS_SECRET_NAME, secret); + + Duration duration = Duration.ofMinutes(30).plusHours(6).plusSeconds(30); + VaultMetadataRequest request = VaultMetadataRequest.builder().casRequired(true) + .deleteVersionAfter(duration).maxVersions(20).build(); + + vaultKeyValueMetadataOperations.put(CAS_SECRET_NAME, request); + + VaultMetadataResponse metadataResponseAfterUpdate = vaultKeyValueMetadataOperations + .get(CAS_SECRET_NAME); + + assertThat(metadataResponseAfterUpdate.isCasRequired()) + .isEqualTo(request.isCasRequired()); + assertThat(metadataResponseAfterUpdate.getMaxVersions()) + .isEqualTo(request.getMaxVersions()); + assertThat(metadataResponseAfterUpdate.getDeleteVersionAfter()) + .isEqualTo(duration); + } + + @Test + void shouldDeleteMetadata() { + + kvOperations.delete(SECRET_NAME); + VaultMetadataResponse metadataResponse = vaultKeyValueMetadataOperations + .get(SECRET_NAME); + Versioned.Metadata version1 = metadataResponse.getVersions().get(0); + assertThat(version1.getDeletedAt()).isBefore(Instant.now()); + + vaultKeyValueMetadataOperations.delete(SECRET_NAME); + + VaultResponse response = kvOperations.get(SECRET_NAME); + assertThat(response).isNull(); + } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java index 2bd842bb..cfe278bd 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VersionedKeyValueBackendIntegrationTests.java @@ -93,3 +93,4 @@ class VersionedKeyValueBackendIntegrationTests extends IntegrationTestSupport { context.stop(); } } + diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/DurationParserUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/DurationParserUnitTests.java new file mode 100644 index 00000000..87a31e6e --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/DurationParserUnitTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DurationParser}. + * + * @author Mark Paluch + */ +class DurationParserUnitTests { + + @Test + void shouldParseSimpleDuration() { + + assertThat(DurationParser.parseDuration("0s")).isEqualTo(Duration.ZERO); + assertThat(DurationParser.parseDuration("0h")).isEqualTo(Duration.ZERO); + assertThat(DurationParser.parseDuration("0m")).isEqualTo(Duration.ZERO); + assertThat(DurationParser.parseDuration("1s")).isEqualTo(Duration.ofSeconds(1)); + assertThat(DurationParser.parseDuration("1h")).isEqualTo(Duration.ofHours(1)); + } + + @Test + void shouldParseComplexDuration() { + + Duration duration = Duration.ofMinutes(30).plusHours(6).plusSeconds(30) + .plusMillis(100).plusNanos(1100); + + assertThat(DurationParser.parseDuration("6h30m30s100ms1us100ns")) + .isEqualTo(duration); + assertThat(DurationParser.parseDuration("23430s100001100ns")).isEqualTo(duration); + } + + @Test + void shouldFormatComplexDuration() { + + Duration duration = Duration.ofMinutes(30).plusHours(6).plusSeconds(30) + .plusMillis(100).plusNanos(1100); + + String result = DurationParser.formatDuration(duration); + + assertThat(result).isEqualTo("23430s100001100ns"); + } +} diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 63eb480c..3f23391b 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -6,6 +6,7 @@ * Support for PEM-encoded certificates for keystore and truststore usage. * `ReactiveVaultEndpointProvider` for non-blocking lookup of `VaultEndpoint`. +* `VaultKeyValueMetadataOperations` for Key-Value metadata interaction. [[new-features.2-2-0]] === What's new in Spring Vault 2.2