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.
This commit is contained in:
Mark Paluch
2020-05-28 09:12:41 +02:00
parent 034185bb17
commit 6413a20c7e
9 changed files with 710 additions and 374 deletions

View File

@@ -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 <a href="https://www.vaultproject.io/api-docs/secret/kv/kv-v2#update-metadata">kv backend metadata api docs</a>
* @see <a href=
* "https://www.vaultproject.io/api-docs/secret/kv/kv-v2#update-metadata">Key-Value
* Metadata API</a>
* @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);
}

View File

@@ -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<String, Object> metadataResponse =
vaultOperations.read("/" + this.basePath + "/metadata/" + path, Map.class).getData();
VaultResponseSupport<Map> 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<String, Object> metadataResponse) {
return VaultMetadataResponse.builder()
.casRequired(Boolean.parseBoolean(String.valueOf(metadataResponse.get("cas_required"))))
.createdTime(toInstant(metadataResponse.get("created_time")))
.currentVersion(Integer.parseInt(String.valueOf(metadataResponse.get("current_version"))))
.deleteVersionAfter(String.valueOf(metadataResponse.get("delete_version_after")))
.maxVersions(Integer.parseInt(String.valueOf(metadataResponse.get("max_versions"))))
.oldestVersion(Integer.parseInt(String.valueOf(metadataResponse.get("oldest_version"))))
.updatedTime(toInstant(metadataResponse.get("updated_time")))
.versions(buildVersions(metadataResponse.get("versions")))
.build();
}
Assert.hasText(path, "Path must not be empty");
Assert.notNull(body, "Body must not be null");
private static List<Versioned.Metadata> 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<String, Object> 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<Versioned.Metadata> buildVersions(
Map<String, Map<String, Object>> versions) {
return versions.entrySet().stream()
.map(entry -> buildVersion(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
private static Versioned.Metadata buildVersion(String version,
Map<String, Object> 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;
}
}

View File

@@ -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 <a href="https://golang.org/pkg/time/#ParseDuration">Go ParseDuration</a>
*/
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();
}
}

View File

@@ -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 <a href="https://www.vaultproject.io/api-docs/secret/kv/kv-v2#update-metadata">Update Metadata</a>
* @see <a href=
* "https://www.vaultproject.io/api-docs/secret/kv/kv-v2#update-metadata">Update
* Metadata</a>
* @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 <a href="https://golang.org/pkg/time/#ParseDuration">Go duration format string</a>.
*/
public String getDeleteVersionAfter() {
return deleteVersionAfter;
}
/**
* @return the deletion_time for all new versions written to this key. Accepts
* <a href="https://golang.org/pkg/time/#ParseDuration">Go duration format string</a>.
*/
public String getDeleteVersionAfter() {
return 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 <a href="https://golang.org/pkg/time/#ParseDuration">Go duration format string</a>.
*
* @param deleteVersionAfter
* @return {@link VaultMetadataRequest}
*/
public VaultMetadataRequestBuilder deleteVersionAfter(String deleteVersionAfter) {
this.deleteVersionAfter = deleteVersionAfter;
return this;
}
/**
* 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);
}
}
}

View File

@@ -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<Versioned.Metadata> versions;
private final List<Versioned.Metadata> versions;
VaultMetadataResponse(boolean casRequired, Instant createdTime, int currentVersion, String deleteVersionAfter,
int maxVersions, int oldestVersion, Instant updatedTime, List<Versioned.Metadata> 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<Versioned.Metadata> 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 <a href="https://golang.org/pkg/time/#ParseDuration">Go duration format string</a>.
*/
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<Versioned.Metadata> 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<Versioned.Metadata> 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<Versioned.Metadata> versions;
private boolean casRequired;
private Instant createdTime;
private int currentVersion;
private String deleteVersionAfter;
private int maxVersions;
private int oldestVersion;
private Instant updatedTime;
private List<Versioned.Metadata> 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<Versioned.Metadata> versions) {
this.versions = versions;
return this;
}
public VaultMetadataResponseBuilder versions(List<Versioned.Metadata> 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);
}
}
}

View File

@@ -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<String, Object> 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<String, Object> secret = new HashMap<>();
secret.put("key", "value");
@Test
public void shouldUpdateMetadataVersions() {
Map<String, Object> 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<String, Object> 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<String, Object> 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();
}
}

View File

@@ -93,3 +93,4 @@ class VersionedKeyValueBackendIntegrationTests extends IntegrationTestSupport {
context.stop();
}
}

View File

@@ -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");
}
}

View File

@@ -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