Implement writing of KV v2 secret data (#16)
This commit is contained in:
parent
e4cf8a1dde
commit
ab33325b8e
@ -334,7 +334,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
payload.put("value", policy);
|
||||
payload.put("display_name", displayName);
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.postWithoutResponse(PATH_AUTH_APPID + "map/app-id/" + appID, payload, token);
|
||||
|
||||
return true;
|
||||
@ -347,7 +347,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
Map<String, String> payload = new HashMap<>();
|
||||
payload.put("value", appID);
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.postWithoutResponse(PATH_AUTH_APPID + "map/user-id/" + userID, payload, token);
|
||||
|
||||
return true;
|
||||
@ -357,7 +357,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
public final boolean createAppRole(final AppRole role) throws VaultConnectorException {
|
||||
requireAuth();
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.postWithoutResponse(String.format(PATH_AUTH_APPROLE_ROLE, role.getName(), ""), role, token);
|
||||
|
||||
/* Set custom ID if provided */
|
||||
@ -375,7 +375,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
public final boolean deleteAppRole(final String roleName) throws VaultConnectorException {
|
||||
requireAuth();
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.deleteWithoutResponse(String.format(PATH_AUTH_APPROLE_ROLE, roleName, ""), token);
|
||||
|
||||
return true;
|
||||
@ -400,7 +400,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
Map<String, String> payload = new HashMap<>();
|
||||
payload.put("role_id", roleID);
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.postWithoutResponse(String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/role-id"), payload, token);
|
||||
|
||||
return true;
|
||||
@ -446,7 +446,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
throws VaultConnectorException {
|
||||
requireAuth();
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.postWithoutResponse(
|
||||
String.format(PATH_AUTH_APPROLE_ROLE, roleName, "/secret-id/destroy"),
|
||||
new AppRoleSecret(secretID),
|
||||
@ -504,6 +504,28 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
return request.get(mount + PATH_METADATA + key, new HashMap<>(), token, MetadataResponse.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final SecretVersionResponse writeSecretData(final String mount, final String key, final Map<String, Object> data, final Integer cas) throws VaultConnectorException {
|
||||
requireAuth();
|
||||
|
||||
if (key == null || key.isEmpty()) {
|
||||
throw new InvalidRequestException("Secret path must not be empty.");
|
||||
}
|
||||
|
||||
// Add CAS value to options map if present.
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
if (cas != null) {
|
||||
options.put("cas", cas);
|
||||
}
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("data", data);
|
||||
payload.put("options", options);
|
||||
|
||||
/* Issue request and parse metadata response */
|
||||
return request.post(mount + PATH_DATA + key, payload, token, SecretVersionResponse.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final List<String> list(final String path) throws VaultConnectorException {
|
||||
requireAuth();
|
||||
@ -532,7 +554,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
payload = payloadMap;
|
||||
}
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.postWithoutResponse(key, payload, token);
|
||||
}
|
||||
|
||||
@ -540,7 +562,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
public final void delete(final String key) throws VaultConnectorException {
|
||||
requireAuth();
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.deleteWithoutResponse(key, token);
|
||||
}
|
||||
|
||||
@ -586,7 +608,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("versions", versions);
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.postWithoutResponse(mount + pathPart + key, payload, token);
|
||||
}
|
||||
|
||||
@ -594,7 +616,7 @@ public class HTTPVaultConnector implements VaultConnector {
|
||||
public final void revoke(final String leaseID) throws VaultConnectorException {
|
||||
requireAuth();
|
||||
|
||||
/* Issue request anx expect code 204 with empty response */
|
||||
/* Issue request and expect code 204 with empty response */
|
||||
request.putWithoutResponse(PATH_REVOKE + leaseID, new HashMap<>(), token);
|
||||
}
|
||||
|
||||
|
@ -435,6 +435,49 @@ public interface VaultConnector extends AutoCloseable, Serializable {
|
||||
return readSecretVersion(mount, key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write secret to Vault.
|
||||
* Prefix "secret/" is automatically added to path. Only available for KV v2 secrets.
|
||||
*
|
||||
* @param key Secret identifier.
|
||||
* @param data Secret content. Value must be be JSON serializable.
|
||||
* @return Metadata for the created/updated secret.
|
||||
* @throws VaultConnectorException on error
|
||||
* @since 0.8
|
||||
*/
|
||||
default SecretVersionResponse writeSecretData(final String key, final Map<String, Object> data) throws VaultConnectorException {
|
||||
return writeSecretData(PATH_SECRET, key, data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write secret to Vault.
|
||||
* Prefix "secret/" is automatically added to path. Only available for KV v2 secrets.
|
||||
*
|
||||
* @param mount Secret store mountpoint (without leading or trailing slash).
|
||||
* @param key Secret identifier
|
||||
* @param data Secret content. Value must be be JSON serializable.
|
||||
* @return Metadata for the created/updated secret.
|
||||
* @throws VaultConnectorException on error
|
||||
* @since 0.8
|
||||
*/
|
||||
default SecretVersionResponse writeSecretData(final String mount, final String key, final Map<String, Object> data) throws VaultConnectorException {
|
||||
return writeSecretData(mount, key, data, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write secret to Vault.
|
||||
* Prefix "secret/" is automatically added to path. Only available for KV v2 secrets.
|
||||
*
|
||||
* @param mount Secret store mountpoint (without leading or trailing slash).
|
||||
* @param key Secret identifier
|
||||
* @param data Secret content. Value must be be JSON serializable.
|
||||
* @param cas Use Check-And-Set operation, i.e. only allow writing if current version matches this value.
|
||||
* @return Metadata for the created/updated secret.
|
||||
* @throws VaultConnectorException on error
|
||||
* @since 0.8
|
||||
*/
|
||||
SecretVersionResponse writeSecretData(final String mount, final String key, final Map<String, Object> data, final Integer cas) throws VaultConnectorException;
|
||||
|
||||
/**
|
||||
* Retrieve secret data from Vault.
|
||||
* Prefix "secret/data" is automatically added to key. Only available for KV v2 secrets.
|
||||
@ -456,7 +499,7 @@ public interface VaultConnector extends AutoCloseable, Serializable {
|
||||
* @param mount Secret store mountpoint (without leading or trailing slash).
|
||||
* @param key Secret identifier
|
||||
* @param version Version to read. If {@code null} or zero, the latest version will be returned.
|
||||
* @return Secret response
|
||||
* @return Secret responsef
|
||||
* @throws VaultConnectorException on error
|
||||
* @since 0.8
|
||||
*/
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.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.Map;
|
||||
|
||||
/**
|
||||
* Vault response for a single secret version metatada, i.e. after update (KV v2).
|
||||
*
|
||||
* @author Stefan Kalscheuer
|
||||
* @since 0.8
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SecretVersionResponse extends VaultDataResponse {
|
||||
|
||||
private VersionMetadata 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), VersionMetadata.class);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidResponseException("Failed deserializing response", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual metadata.
|
||||
*
|
||||
* @return Metadata.
|
||||
*/
|
||||
public VersionMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
@ -32,9 +32,7 @@ import java.io.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.apache.commons.io.FileUtils.copyDirectory;
|
||||
@ -82,6 +80,8 @@ public class HTTPVaultConnectorTest {
|
||||
private static final String SECRET2_KEY = "foo2";
|
||||
private static final String SECRET2_VALUE1 = "bar2";
|
||||
private static final String SECRET2_VALUE2 = "bar3";
|
||||
private static final String SECRET2_VALUE3 = "bar4";
|
||||
private static final String SECRET2_VALUE4 = "bar4";
|
||||
|
||||
private Process vaultProcess;
|
||||
private VaultConnector connector;
|
||||
@ -766,6 +766,62 @@ public class HTTPVaultConnectorTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test writing of secrets to KV v2 store.
|
||||
*/
|
||||
@Test
|
||||
public void writeSecretV2Test() {
|
||||
authUser();
|
||||
assumeTrue(connector.isAuthorized());
|
||||
|
||||
// First get the current version of the secret.
|
||||
int currentVersion = -1;
|
||||
try {
|
||||
MetadataResponse res = connector.readSecretMetadata(MOUNT_KV2, SECRET2_KEY);
|
||||
currentVersion = res.getMetadata().getCurrentVersion();
|
||||
} catch (VaultConnectorException e) {
|
||||
fail("Reading secret metadata failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Now write (update) the data and verify the version.
|
||||
try {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("value", SECRET2_VALUE3);
|
||||
SecretVersionResponse res = connector.writeSecretData(MOUNT_KV2, SECRET2_KEY, data);
|
||||
assertThat("Version not updated after writing secret", res.getMetadata().getVersion(), is(currentVersion + 1));
|
||||
currentVersion = res.getMetadata().getVersion();
|
||||
} catch (VaultConnectorException e) {
|
||||
fail("Writing secret to KV v2 store failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Verify the content.
|
||||
try {
|
||||
SecretResponse res = connector.readSecretData(MOUNT_KV2, SECRET2_KEY);
|
||||
assertThat("Data not updated correctly", res.getValue(), is(SECRET2_VALUE3));
|
||||
} catch (VaultConnectorException e) {
|
||||
fail("Reading secret from KV v2 store failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Now try with explicit CAS value (invalid).
|
||||
try {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("value", SECRET2_VALUE4);
|
||||
SecretVersionResponse res = connector.writeSecretData(MOUNT_KV2, SECRET2_KEY, data, currentVersion - 1);
|
||||
fail("Writing secret to KV v2 with invalid CAS value succeeded");
|
||||
} catch (VaultConnectorException e) {
|
||||
assertThat("Unexpected exception", e, is(instanceOf(InvalidResponseException.class)));
|
||||
}
|
||||
|
||||
// And finally with a correct CAS value.
|
||||
try {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("value", SECRET2_VALUE4);
|
||||
SecretVersionResponse res = connector.writeSecretData(MOUNT_KV2, SECRET2_KEY, data, currentVersion);
|
||||
} catch (VaultConnectorException e) {
|
||||
fail("Writing secret to KV v2 with correct CAS value failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reading of secret metadata from KV v2 store.
|
||||
*/
|
||||
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* JUnit Test for {@link SecretVersionResponse} model.
|
||||
*
|
||||
* @author Stefan Kalscheuer
|
||||
* @since 0.8
|
||||
*/
|
||||
public class SecretVersionResponseTest {
|
||||
private static final String CREATION_TIME = "2018-03-22T02:24:06.945319214Z";
|
||||
private static final String DELETION_TIME = "2018-03-22T02:36:43.986212308Z";
|
||||
private static final Integer VERSION = 42;
|
||||
|
||||
private static final String META_JSON = "{\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"created_time\": \"" + CREATION_TIME + "\",\n" +
|
||||
" \"deletion_time\": \"" + DELETION_TIME + "\",\n" +
|
||||
" \"destroyed\": false,\n" +
|
||||
" \"version\": " + VERSION + "\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
|
||||
/**
|
||||
* Test creation from JSON value as returned by Vault (JSON example copied from Vault documentation).
|
||||
*/
|
||||
@Test
|
||||
public void jsonRoundtrip() {
|
||||
try {
|
||||
SecretVersionResponse res = new ObjectMapper().readValue(META_JSON, SecretVersionResponse.class);
|
||||
assertThat("Parsed response is NULL", res, is(notNullValue()));
|
||||
assertThat("Parsed metadatra is NULL", res.getMetadata(), is(notNullValue()));
|
||||
assertThat("Incorrect created time", res.getMetadata().getCreatedTimeString(), is(CREATION_TIME));
|
||||
assertThat("Incorrect deletion time", res.getMetadata().getDeletionTimeString(), is(DELETION_TIME));
|
||||
assertThat("Incorrect destroyed state", res.getMetadata().isDestroyed(), is(false));
|
||||
assertThat("Incorrect version", res.getMetadata().getVersion(), is(VERSION));
|
||||
} catch (IOException e) {
|
||||
fail("SecretVersionResponse deserialization failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user