Merge pull request #7294 from vsevel/write_secret

Add Vault write and delete secret Fixes #7155
This commit is contained in:
sberyozkin
2020-02-21 23:36:20 +00:00
committed by GitHub
12 changed files with 238 additions and 3 deletions

View File

@@ -20,4 +20,21 @@ public interface VaultKVSecretEngine {
*/ */
Map<String, String> readSecret(String path); Map<String, String> readSecret(String path);
/**
* Writes the secret at the given path. If the path does not exist, the secret will
* be created. If not the new secret will be merged with the existing one.
*
* @param path in Vault, without the kv engine mount path
* @param secret to write at path
*/
void writeSecret(String path, Map<String, String> secret);
/**
* Deletes the secret at the given path. It has no effect if no secret is currently
* stored at path.
*
* @param path to delete
*/
void deleteSecret(String path);
} }

View File

@@ -4,6 +4,9 @@ import java.util.Map;
import io.quarkus.vault.VaultKVSecretEngine; import io.quarkus.vault.VaultKVSecretEngine;
import io.quarkus.vault.runtime.client.VaultClient; import io.quarkus.vault.runtime.client.VaultClient;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV1;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2WriteBody;
import io.quarkus.vault.runtime.config.VaultRuntimeConfig; import io.quarkus.vault.runtime.config.VaultRuntimeConfig;
public class VaultKvManager implements VaultKVSecretEngine { public class VaultKvManager implements VaultKVSecretEngine {
@@ -25,10 +28,38 @@ public class VaultKvManager implements VaultKVSecretEngine {
String mount = serverConfig.kvSecretEngineMountPath; String mount = serverConfig.kvSecretEngineMountPath;
if (serverConfig.kvSecretEngineVersion == 1) { if (serverConfig.kvSecretEngineVersion == 1) {
return vaultClient.getSecretV1(clientToken, mount, path).data; VaultKvSecretV1 secretV1 = vaultClient.getSecretV1(clientToken, mount, path);
return secretV1.data;
} else { } else {
return vaultClient.getSecretV2(clientToken, mount, path).data.data; VaultKvSecretV2 secretV2 = vaultClient.getSecretV2(clientToken, mount, path);
return secretV2.data.data;
} }
} }
@Override
public void writeSecret(String path, Map<String, String> secret) {
String clientToken = vaultAuthManager.getClientToken();
String mount = serverConfig.kvSecretEngineMountPath;
if (serverConfig.kvSecretEngineVersion == 1) {
vaultClient.writeSecretV1(clientToken, mount, path, secret);
} else {
VaultKvSecretV2WriteBody body = new VaultKvSecretV2WriteBody();
body.data = secret;
vaultClient.writeSecretV2(clientToken, mount, path, body);
}
}
@Override
public void deleteSecret(String path) {
String clientToken = vaultAuthManager.getClientToken();
String mount = serverConfig.kvSecretEngineMountPath;
if (serverConfig.kvSecretEngineVersion == 1) {
vaultClient.deleteSecretV1(clientToken, mount, path);
} else {
vaultClient.deleteSecretV2(clientToken, mount, path);
}
}
} }

View File

@@ -6,6 +6,7 @@ import static io.quarkus.vault.runtime.client.OkHttpClientFactory.createHttpClie
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Map;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@@ -25,6 +26,8 @@ import io.quarkus.vault.runtime.client.dto.auth.VaultUserPassAuthBody;
import io.quarkus.vault.runtime.client.dto.database.VaultDatabaseCredentials; import io.quarkus.vault.runtime.client.dto.database.VaultDatabaseCredentials;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV1; import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV1;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2; import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2Write;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2WriteBody;
import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesBody; import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesBody;
import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup; import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup;
import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease; import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease;
@@ -88,6 +91,26 @@ public class OkHttpVaultClient implements VaultClient {
return get(secretEnginePath + "/data/" + path, token, VaultKvSecretV2.class); return get(secretEnginePath + "/data/" + path, token, VaultKvSecretV2.class);
} }
@Override
public void writeSecretV1(String token, String secretEnginePath, String path, Map<String, String> secret) {
post(secretEnginePath + "/" + path, token, secret, null, 204);
}
@Override
public void writeSecretV2(String token, String secretEnginePath, String path, VaultKvSecretV2WriteBody body) {
post(secretEnginePath + "/data/" + path, token, body, VaultKvSecretV2Write.class);
}
@Override
public void deleteSecretV1(String token, String secretEnginePath, String path) {
delete(secretEnginePath + "/" + path, token, null, null, 204);
}
@Override
public void deleteSecretV2(String token, String secretEnginePath, String path) {
delete(secretEnginePath + "/data/" + path, token, null, null, 204);
}
@Override @Override
public VaultRenewSelf renewSelf(String token, String increment) { public VaultRenewSelf renewSelf(String token, String increment) {
VaultRenewSelfBody body = new VaultRenewSelfBody(increment); VaultRenewSelfBody body = new VaultRenewSelfBody(increment);
@@ -146,6 +169,11 @@ public class OkHttpVaultClient implements VaultClient {
// --- // ---
protected <T> T delete(String path, String token, Object body, Class<T> resultClass, int expectedCode) {
Request request = builder(path, token).delete(requestBody(body)).build();
return exec(request, resultClass, expectedCode);
}
protected <T> T post(String path, String token, Object body, Class<T> resultClass, int expectedCode) { protected <T> T post(String path, String token, Object body, Class<T> resultClass, int expectedCode) {
Request request = builder(path, token).post(requestBody(body)).build(); Request request = builder(path, token).post(requestBody(body)).build();
return exec(request, resultClass, expectedCode); return exec(request, resultClass, expectedCode);

View File

@@ -1,5 +1,7 @@
package io.quarkus.vault.runtime.client; package io.quarkus.vault.runtime.client;
import java.util.Map;
import io.quarkus.vault.runtime.client.dto.auth.VaultAppRoleAuth; import io.quarkus.vault.runtime.client.dto.auth.VaultAppRoleAuth;
import io.quarkus.vault.runtime.client.dto.auth.VaultKubernetesAuth; import io.quarkus.vault.runtime.client.dto.auth.VaultKubernetesAuth;
import io.quarkus.vault.runtime.client.dto.auth.VaultLookupSelf; import io.quarkus.vault.runtime.client.dto.auth.VaultLookupSelf;
@@ -8,6 +10,7 @@ import io.quarkus.vault.runtime.client.dto.auth.VaultUserPassAuth;
import io.quarkus.vault.runtime.client.dto.database.VaultDatabaseCredentials; import io.quarkus.vault.runtime.client.dto.database.VaultDatabaseCredentials;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV1; import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV1;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2; import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2;
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2WriteBody;
import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup; import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup;
import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease; import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecrypt; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecrypt;
@@ -43,6 +46,14 @@ public interface VaultClient {
VaultKvSecretV2 getSecretV2(String token, String secretEnginePath, String path); VaultKvSecretV2 getSecretV2(String token, String secretEnginePath, String path);
void writeSecretV1(String token, String secretEnginePath, String path, Map<String, String> values);
void writeSecretV2(String token, String secretEnginePath, String path, VaultKvSecretV2WriteBody body);
void deleteSecretV1(String token, String secretEnginePath, String path);
void deleteSecretV2(String token, String secretEnginePath, String path);
VaultDatabaseCredentials generateDatabaseCredentials(String token, String databaseCredentialsRole); VaultDatabaseCredentials generateDatabaseCredentials(String token, String databaseCredentialsRole);
VaultTransitEncrypt encrypt(String token, String keyName, VaultTransitEncryptBody body); VaultTransitEncrypt encrypt(String token, String keyName, VaultTransitEncryptBody body);

View File

@@ -0,0 +1,10 @@
package io.quarkus.vault.runtime.client.dto.kv;
import io.quarkus.vault.runtime.client.dto.AbstractVaultDTO;
/**
* {"request_id":"89ce65f0-e494-cfea-975e-6029d235614e","lease_id":"","renewable":false,"lease_duration":0,"data":{"created_time":"2020-02-19T21:18:06.0367901Z","deletion_time":"","destroyed":false,"version":1},"wrap_info":null,"warnings":null,"auth":null}
*/
public class VaultKvSecretV2Write extends AbstractVaultDTO<VaultKvSecretV2WriteData, Object> {
}

View File

@@ -0,0 +1,12 @@
package io.quarkus.vault.runtime.client.dto.kv;
import java.util.Map;
import io.quarkus.vault.runtime.client.dto.VaultModel;
public class VaultKvSecretV2WriteBody implements VaultModel {
public Map<String, Integer> options;
public Map<String, String> data;
}

View File

@@ -0,0 +1,15 @@
package io.quarkus.vault.runtime.client.dto.kv;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.quarkus.vault.runtime.client.dto.VaultModel;
public class VaultKvSecretV2WriteData implements VaultModel {
@JsonProperty("created_time")
public String createdTime;
@JsonProperty("deletion_time")
public String deletionTime;
public boolean destroyed;
public int version;
}

View File

@@ -0,0 +1,37 @@
package io.quarkus.vault;
import javax.inject.Inject;
import org.jboss.logging.Logger;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.RegisterExtension;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.vault.test.VaultTestExtension;
import io.quarkus.vault.test.VaultTestLifecycleManager;
@DisabledOnOs(OS.WINDOWS) // https://github.com/quarkusio/quarkus/issues/3796
@QuarkusTestResource(VaultTestLifecycleManager.class)
public class VaultKv2ITCase {
private static final Logger log = Logger.getLogger(VaultKv2ITCase.class.getName());
public static final String CRUD_PATH = "crud";
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer(
() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource("application-vault-kv-version2-datasource.properties", "application.properties"));
@Inject
VaultKVSecretEngine kvSecretEngine;
@Test
public void crudSecretV2() {
VaultTestExtension.assertCrudSecret(kvSecretEngine);
}
}

View File

@@ -16,6 +16,7 @@ import org.jboss.logging.Logger;
import io.quarkus.vault.VaultKVSecretEngine; import io.quarkus.vault.VaultKVSecretEngine;
import io.quarkus.vault.VaultTransitSecretEngine; import io.quarkus.vault.VaultTransitSecretEngine;
import io.quarkus.vault.runtime.client.VaultClientException;
import io.quarkus.vault.transit.ClearData; import io.quarkus.vault.transit.ClearData;
import io.quarkus.vault.transit.SigningInput; import io.quarkus.vault.transit.SigningInput;
@@ -48,12 +49,29 @@ public class VaultTestService {
return "password=" + password + "; expected: " + expectedPassword; return "password=" + password + "; expected: " + expectedPassword;
} }
// basic
Map<String, String> secrets = kv.readSecret("foo"); Map<String, String> secrets = kv.readSecret("foo");
String expectedSecrets = "{secret=s\u20accr\u20act}"; String expectedSecrets = "{secret=s\u20accr\u20act}";
if (!expectedSecrets.equals(secrets.toString())) { if (!expectedSecrets.equals(secrets.toString())) {
return "/foo=" + secrets + "; expected: " + expectedSecrets; return "/foo=" + secrets + "; expected: " + expectedSecrets;
} }
// crud
kv.writeSecret("crud", secrets);
secrets = kv.readSecret("crud");
if (!expectedSecrets.equals(secrets.toString())) {
return "/crud=" + secrets + "; expected: " + expectedSecrets;
}
kv.deleteSecret("crud");
try {
secrets = kv.readSecret("crud");
return "/crud=" + secrets + "; expected 404";
} catch (VaultClientException e) {
if (e.getStatus() != 404) {
return "http response code=" + e.getStatus() + "; expected: 404";
}
}
try { try {
List gifts = entityManager.createQuery("select g from Gift g").getResultList(); List gifts = entityManager.createQuery("select g from Gift g").getResultList();
int count = gifts.size(); int count = gifts.size();

View File

@@ -66,6 +66,7 @@ import io.quarkus.vault.runtime.client.dto.transit.VaultTransitVerify;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitVerifyBatchInput; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitVerifyBatchInput;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitVerifyBody; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitVerifyBody;
import io.quarkus.vault.runtime.config.VaultAuthenticationType; import io.quarkus.vault.runtime.config.VaultAuthenticationType;
import io.quarkus.vault.test.VaultTestExtension;
import io.quarkus.vault.test.VaultTestLifecycleManager; import io.quarkus.vault.test.VaultTestLifecycleManager;
import io.quarkus.vault.test.client.TestVaultClient; import io.quarkus.vault.test.client.TestVaultClient;
import io.quarkus.vault.test.client.dto.VaultTransitHash; import io.quarkus.vault.test.client.dto.VaultTransitHash;
@@ -79,6 +80,8 @@ public class VaultITCase {
public static final String MY_PASSWORD = "my-password"; public static final String MY_PASSWORD = "my-password";
public static final String CRUD_PATH = "crud";
@RegisterExtension @RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest() static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
@@ -130,6 +133,11 @@ public class VaultITCase {
assertEquals("{" + SECRET_KEY + "=" + SECRET_VALUE + "}", secrets.toString()); assertEquals("{" + SECRET_KEY + "=" + SECRET_VALUE + "}", secrets.toString());
} }
@Test
public void crudSecretV1() {
VaultTestExtension.assertCrudSecret(kvSecretEngine);
}
@Test @Test
public void httpclient() { public void httpclient() {

View File

@@ -20,7 +20,10 @@ import java.sql.Statement;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.TreeMap;
import javax.sql.DataSource; import javax.sql.DataSource;
@@ -32,6 +35,7 @@ import org.testcontainers.containers.Network;
import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.PostgreSQLContainer;
import io.quarkus.vault.VaultException; import io.quarkus.vault.VaultException;
import io.quarkus.vault.VaultKVSecretEngine;
import io.quarkus.vault.runtime.VaultManager; import io.quarkus.vault.runtime.VaultManager;
import io.quarkus.vault.runtime.client.VaultClientException; import io.quarkus.vault.runtime.client.VaultClientException;
import io.quarkus.vault.runtime.config.VaultRuntimeConfig; import io.quarkus.vault.runtime.config.VaultRuntimeConfig;
@@ -76,6 +80,8 @@ public class VaultTestExtension {
public static final String TMP_POSTGRES_INIT_SQL_FILE = "/tmp/postgres-init.sql"; public static final String TMP_POSTGRES_INIT_SQL_FILE = "/tmp/postgres-init.sql";
public static final String TEST_QUERY_STRING = "SELECT 1"; public static final String TEST_QUERY_STRING = "SELECT 1";
private static String CRUD_PATH = "crud";
public GenericContainer vaultContainer; public GenericContainer vaultContainer;
public PostgreSQLContainer postgresContainer; public PostgreSQLContainer postgresContainer;
public String rootToken = null; public String rootToken = null;
@@ -98,6 +104,40 @@ public class VaultTestExtension {
} }
} }
public static void assertCrudSecret(VaultKVSecretEngine kvSecretEngine) {
assertDeleteSecret(kvSecretEngine);
assertDeleteSecret(kvSecretEngine);
Map<String, String> newsecrets = new HashMap<>();
newsecrets.put("first", "one");
newsecrets.put("second", "two");
kvSecretEngine.writeSecret(CRUD_PATH, newsecrets);
assertEquals("{first=one, second=two}", readSecretAsString(kvSecretEngine, CRUD_PATH));
newsecrets.put("first", "un");
newsecrets.put("third", "tres");
kvSecretEngine.writeSecret(CRUD_PATH, newsecrets);
assertEquals("{first=un, second=two, third=tres}", readSecretAsString(kvSecretEngine, CRUD_PATH));
assertDeleteSecret(kvSecretEngine);
}
private static void assertDeleteSecret(VaultKVSecretEngine kvSecretEngine) {
kvSecretEngine.deleteSecret(CRUD_PATH);
try {
readSecretAsString(kvSecretEngine, CRUD_PATH);
} catch (VaultClientException e) {
assertEquals(404, e.getStatus());
}
}
private static String readSecretAsString(VaultKVSecretEngine kvSecretEngine, String path) {
Map<String, String> secret = kvSecretEngine.readSecret(path);
return new TreeMap<>(secret).toString();
}
private static VaultManager createVaultManager() { private static VaultManager createVaultManager() {
VaultRuntimeConfig serverConfig = new VaultRuntimeConfig(); VaultRuntimeConfig serverConfig = new VaultRuntimeConfig();
serverConfig.tls = new VaultTlsConfig(); serverConfig.tls = new VaultTlsConfig();

View File

@@ -41,3 +41,11 @@ path "transit/*" {
#path "transit/sign/my-sign-key" { #path "transit/sign/my-sign-key" {
# capabilities = [ "read", "update" ] # capabilities = [ "read", "update" ]
#} #}
path "secret/crud" {
capabilities = ["read", "create", "update", "delete"]
}
path "secret-v2/data/crud" {
capabilities = ["read", "create", "update", "delete"]
}