Merge branch 'feature/16-kv_v2_support' into develop

This commit is contained in:
2019-03-17 14:05:56 +01:00
10 changed files with 835 additions and 14 deletions

View File

@ -74,6 +74,11 @@ public class HTTPVaultConnector implements VaultConnector {
private static final String PATH_AUTH_APPROLE_ROLE = "auth/approle/role/%s%s";
private static final String PATH_REVOKE = "sys/leases/revoke/";
private static final String PATH_HEALTH = "sys/health";
private static final String PATH_DATA = "/data/";
private static final String PATH_METADATA = "/metadata/";
private static final String PATH_DELETE = "/delete/";
private static final String PATH_UNDELETE = "/undelete/";
private static final String PATH_DESTROY = "/destroy/";
private static final String HEADER_VAULT_TOKEN = "X-Vault-Token";
@ -620,6 +625,44 @@ public class HTTPVaultConnector implements VaultConnector {
}
}
@Override
public final SecretResponse readSecretVersion(final String key, final Integer version) throws VaultConnectorException {
if (!isAuthorized()) {
throw new AuthorizationRequiredException();
}
/* Request HTTP response and parse secret metadata */
try {
Map<String, String> args = new HashMap<>();
if (version != null) {
args.put("version", version.toString());
}
String response = requestGet(PATH_SECRET + PATH_DATA + key, args);
return jsonMapper.readValue(response, SecretResponse.class);
} catch (IOException e) {
throw new InvalidResponseException(Error.PARSE_RESPONSE, e);
} catch (URISyntaxException ignored) {
/* this should never occur and may leak sensible information */
throw new InvalidRequestException(Error.URI_FORMAT);
}
}
@Override
public final MetadataResponse readSecretMetadata(final String key) throws VaultConnectorException {
if (!isAuthorized()) {
throw new AuthorizationRequiredException();
}
/* Request HTTP response and parse secret metadata */
try {
String response = requestGet(PATH_SECRET + PATH_METADATA + key, new HashMap<>());
return jsonMapper.readValue(response, MetadataResponse.class);
} catch (IOException e) {
throw new InvalidResponseException(Error.PARSE_RESPONSE, e);
} catch (URISyntaxException ignored) {
/* this should never occur and may leak sensible information */
throw new InvalidRequestException(Error.URI_FORMAT);
}
}
@Override
public final List<String> list(final String path) throws VaultConnectorException {
if (!isAuthorized()) {
@ -639,7 +682,7 @@ public class HTTPVaultConnector implements VaultConnector {
}
@Override
public final void write(final String key, final Map<String, Object> data) throws VaultConnectorException {
public final void write(final String key, final Map<String, Object> data, final Map<String, Object> options) throws VaultConnectorException {
if (!isAuthorized()) {
throw new AuthorizationRequiredException();
}
@ -648,7 +691,18 @@ public class HTTPVaultConnector implements VaultConnector {
throw new InvalidRequestException("Secret path must not be empty.");
}
if (!requestPost(key, data).isEmpty()) {
// By default data is directly passed as payload.
Object payload = data;
// If options are given, split payload in two parts.
if (options != null) {
Map<String, Object> payloadMap = new HashMap<>();
payloadMap.put("data", data);
payloadMap.put("options", options);
payload = payloadMap;
}
if (!requestPost(key, payload).isEmpty()) {
throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE);
}
}
@ -668,6 +722,56 @@ public class HTTPVaultConnector implements VaultConnector {
}
}
@Override
public final void deleteLatestSecretVersion(final String key) throws VaultConnectorException {
delete(PATH_SECRET + PATH_DATA + key);
}
@Override
public final void deleteAllSecretVersions(final String key) throws VaultConnectorException {
delete(PATH_SECRET + PATH_METADATA + key);
}
@Override
public final void deleteSecretVersions(final String key, final int... versions) throws VaultConnectorException {
handleSecretVersions(PATH_DELETE, key, versions);
}
@Override
public final void undeleteSecretVersions(final String key, final int... versions) throws VaultConnectorException {
handleSecretVersions(PATH_UNDELETE, key, versions);
}
@Override
public final void destroySecretVersions(final String key, final int... versions) throws VaultConnectorException {
handleSecretVersions(PATH_DESTROY, key, versions);
}
/**
* Common method to bundle secret version operations.
*
* @param pathPart Path part to query.
* @param key Secret key.
* @param versions Versions to handle.
* @throws VaultConnectorException on error
* @since 0.8
*/
private void handleSecretVersions(final String pathPart, final String key, final int... versions) throws VaultConnectorException {
if (!isAuthorized()) {
throw new AuthorizationRequiredException();
}
/* Request HTTP response and expect empty result */
Map<String, Object> payload = new HashMap<>();
payload.put("versions", versions);
String response = requestPost(PATH_SECRET + pathPart + key, payload);
/* Response should be code 204 without content */
if (!response.isEmpty()) {
throw new InvalidResponseException(Error.UNEXPECTED_RESPONSE);
}
}
@Override
public final void revoke(final String leaseID) throws VaultConnectorException {
if (!isAuthorized()) {

View File

@ -408,6 +408,42 @@ public interface VaultConnector extends AutoCloseable, Serializable {
return read(PATH_SECRET + "/" + key);
}
/**
* Retrieve the latest secret data for specific version from Vault.
* Prefix "secret/data" is automatically added to key. Only available for KV v2 secrets.
*
* @param key Secret identifier
* @return Secret response
* @throws VaultConnectorException on error
* @since 0.8
*/
default SecretResponse readSecretData(final String key) throws VaultConnectorException {
return readSecretVersion(key, null);
}
/**
* Retrieve secret data from Vault.
* Prefix "secret/data" is automatically added to key. Only available for KV v2 secrets.
*
* @param key Secret identifier
* @param version Version to read. If {@code null} or zero, the latest version will be returned.
* @return Secret response
* @throws VaultConnectorException on error
* @since 0.8
*/
SecretResponse readSecretVersion(final String key, final Integer version) throws VaultConnectorException;
/**
* Retrieve secret metadata from Vault.
* Prefix "secret/metadata" is automatically added to key. Only available for KV v2 secrets.
*
* @param key Secret identifier
* @return Metadata response
* @throws VaultConnectorException on error
* @since 0.8
*/
MetadataResponse readSecretMetadata(final String key) throws VaultConnectorException;
/**
* List available nodes from Vault.
*
@ -452,7 +488,20 @@ public interface VaultConnector extends AutoCloseable, Serializable {
* @throws VaultConnectorException on error
* @since 0.5.0
*/
void write(final String key, final Map<String, Object> data) throws VaultConnectorException;
default void write(final String key, final Map<String, Object> data) throws VaultConnectorException {
write(key, data, null);
}
/**
* Write value to Vault.
*
* @param key Secret path
* @param data Secret content. Value must be be JSON serializable.
* @param options Secret options (optional).
* @throws VaultConnectorException on error
* @since 0.8 {@code options} parameter added
*/
void write(final String key, final Map<String, Object> data, final Map<String, Object> options) throws VaultConnectorException;
/**
* Write secret to Vault.
@ -504,6 +553,59 @@ public interface VaultConnector extends AutoCloseable, Serializable {
delete(PATH_SECRET + "/" + key);
}
/**
* Delete latest version of a secret from Vault.
* Only available for KV v2 stores.
*
* @param key Secret path.
* @throws VaultConnectorException on error
* @since 0.8
*/
void deleteLatestSecretVersion(final String key) throws VaultConnectorException;
/**
* Delete latest version of a secret from Vault.
* Only available for KV v2 stores.
*
* @param key Secret path.
* @throws VaultConnectorException on error
* @since 0.8
*/
void deleteAllSecretVersions(final String key) throws VaultConnectorException;
/**
* Delete secret versions from Vault.
* Only available for KV v2 stores.
*
* @param key Secret path.
* @param versions Versions of the secret to delete.
* @throws VaultConnectorException on error
* @since 0.8
*/
void deleteSecretVersions(final String key, final int... versions) throws VaultConnectorException;
/**
* Undelete (restore) secret versions from Vault.
* Only available for KV v2 stores.
*
* @param key Secret path.
* @param versions Versions of the secret to undelete.
* @throws VaultConnectorException on error
* @since 0.8
*/
void undeleteSecretVersions(final String key, final int... versions) throws VaultConnectorException;
/**
* Destroy secret versions from Vault.
* Only available for KV v2 stores.
*
* @param key Secret path.
* @param versions Versions of the secret to destroy.
* @throws VaultConnectorException on error
* @since 0.8
*/
void destroySecretVersions(final String key, final int... versions) throws VaultConnectorException;
/**
* Revoke given lease immediately.
*

View File

@ -0,0 +1,58 @@
/*
* Copyright 2016-2018 Stefan Kalscheuer
*
* 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
*
* http://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 de.stklcode.jvault.connector.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.stklcode.jvault.connector.exception.InvalidResponseException;
import de.stklcode.jvault.connector.model.response.embedded.SecretMetadata;
import de.stklcode.jvault.connector.model.response.embedded.VersionMetadata;
import java.io.IOException;
import java.util.Map;
/**
* Vault response for secret metadata (KV v2).
*
* @author Stefan Kalscheuer
* @since 0.8
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class MetadataResponse extends VaultDataResponse {
private SecretMetadata metadata;
@Override
public final void setData(final Map<String, Object> data) throws InvalidResponseException {
ObjectMapper mapper = new ObjectMapper();
try {
this.metadata = mapper.readValue(mapper.writeValueAsString(data), SecretMetadata.class);
} catch (IOException e) {
throw new InvalidResponseException("Failed deserializing response", e);
}
}
/**
* Get the actual metadata.
*
* @return Metadata.
*/
public SecretMetadata getMetadata() {
return metadata;
}
}

View File

@ -19,6 +19,7 @@ package de.stklcode.jvault.connector.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.stklcode.jvault.connector.exception.InvalidResponseException;
import de.stklcode.jvault.connector.model.response.embedded.VersionMetadata;
import java.io.IOException;
import java.util.HashMap;
@ -33,10 +34,25 @@ import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
public class SecretResponse extends VaultDataResponse {
private Map<String, Object> data;
private VersionMetadata metadata;
@Override
public final void setData(final Map<String, Object> data) throws InvalidResponseException {
this.data = data;
if (data.size() == 2
&& data.containsKey("data") && data.get("data") instanceof Map
&& data.containsKey("metadata") && data.get("metadata") instanceof Map) {
ObjectMapper mapper = new ObjectMapper();
try {
// This is apparently a KV v2 value.
this.data = (Map<String, Object>) data.get("data");
this.metadata = mapper.readValue(mapper.writeValueAsString(data.get("metadata")), VersionMetadata.class);
} catch (ClassCastException | IOException e) {
throw new InvalidResponseException("Failed deserializing response", e);
}
} else {
// For KV v1 without metadata just store the data map.
this.data = data;
}
}
/**
@ -52,6 +68,16 @@ public class SecretResponse extends VaultDataResponse {
return data;
}
/**
* Get secret metadata. This is only available for KV v2 secrets.
*
* @return Metadata of the secret.
* @since 0.8
*/
public final VersionMetadata getMetadata() {
return metadata;
}
/**
* Get a single value for given key.
*
@ -100,7 +126,7 @@ public class SecretResponse extends VaultDataResponse {
/**
* Get response parsed as JSON.
*
* @param key the key
* @param key the key
* @param type Class to parse response
* @param <T> Class to parse response
* @return Parsed object or {@code null} if absent

View File

@ -0,0 +1,127 @@
/*
* Copyright 2016-2018 Stefan Kalscheuer
*
* 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
*
* http://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 de.stklcode.jvault.connector.model.response.embedded;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Map;
/**
* Embedded metadata for Key-Value v2 secrets.
*
* @author Stefan Kalscheuer
* @since 0.8
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class SecretMetadata {
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSX");
@JsonProperty("created_time")
private String createdTimeString;
@JsonProperty("current_version")
private Integer currentVersion;
@JsonProperty("max_versions")
private Integer maxVersions;
@JsonProperty("oldest_version")
private Integer oldestVersion;
@JsonProperty("updated_time")
private String updatedTime;
@JsonProperty("versions")
private Map<Integer, VersionMetadata> versions;
/**
* @return Time of secret creation as raw string representation.
*/
public String getCreatedTimeString() {
return createdTimeString;
}
/**
* @return Time of secret creation.
*/
public ZonedDateTime getCreatedTime() {
if (createdTimeString != null && !createdTimeString.isEmpty()) {
try {
return ZonedDateTime.parse(createdTimeString, TIME_FORMAT);
} catch (DateTimeParseException e) {
// Ignore.
}
}
return null;
}
/**
* @return Current version number.
*/
public Integer getCurrentVersion() {
return currentVersion;
}
/**
* @return Maximum number of versions.
*/
public Integer getMaxVersions() {
return maxVersions;
}
/**
* @return Oldest available version number.
*/
public Integer getOldestVersion() {
return oldestVersion;
}
/**
* @return Time of secret update as raw string representation.
*/
public String getUpdatedTimeString() {
return updatedTime;
}
/**
* @return Time of secret update..
*/
public ZonedDateTime getUpdatedTime() {
if (updatedTime != null && !updatedTime.isEmpty()) {
try {
return ZonedDateTime.parse(updatedTime, TIME_FORMAT);
} catch (DateTimeParseException e) {
// Ignore.
}
}
return null;
}
/**
* @return Version of the entry.
*/
public Map<Integer, VersionMetadata> getVersions() {
return versions;
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2016-2018 Stefan Kalscheuer
*
* 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
*
* http://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 de.stklcode.jvault.connector.model.response.embedded;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Map;
/**
* Embedded metadata for a single Key-Value v2 version.
*
* @author Stefan Kalscheuer
* @since 0.8
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class VersionMetadata {
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSX");
@JsonProperty("created_time")
private String createdTimeString;
@JsonProperty("deletion_time")
private String deletionTimeString;
@JsonProperty("destroyed")
private boolean destroyed;
@JsonProperty("version")
private Integer version;
/**
* @return Time of secret creation as raw string representation.
*/
public String getCreatedTimeString() {
return createdTimeString;
}
/**
* @return Time of secret creation.
*/
public ZonedDateTime getCreatedTime() {
if (createdTimeString != null && !createdTimeString.isEmpty()) {
try {
return ZonedDateTime.parse(createdTimeString, TIME_FORMAT);
} catch (DateTimeParseException e) {
// Ignore.
}
}
return null;
}
/**
* @return Time for secret deletion as raw string representation.
*/
public String getDeletionTimeString() {
return deletionTimeString;
}
/**
* @return Time for secret deletion.
*/
public ZonedDateTime getDeletionTime() {
if (deletionTimeString != null && !deletionTimeString.isEmpty()) {
try {
return ZonedDateTime.parse(deletionTimeString, TIME_FORMAT);
} catch (DateTimeParseException e) {
// Ignore.
}
}
return null;
}
/**
* @return Whether the secret is destroyed.
*/
public boolean isDestroyed() {
return destroyed;
}
/**
* @return Version of the entry.
*/
public Integer getVersion() {
return version;
}
}