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