diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultKVSecretEngine.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultKVSecretEngine.java index aca9c12f1..36e2c3571 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultKVSecretEngine.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultKVSecretEngine.java @@ -20,4 +20,21 @@ public interface VaultKVSecretEngine { */ Map 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 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); + } diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultKvManager.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultKvManager.java index 847619ed5..9c13f48dd 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultKvManager.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultKvManager.java @@ -4,6 +4,9 @@ import java.util.Map; import io.quarkus.vault.VaultKVSecretEngine; 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; public class VaultKvManager implements VaultKVSecretEngine { @@ -25,10 +28,38 @@ public class VaultKvManager implements VaultKVSecretEngine { String mount = serverConfig.kvSecretEngineMountPath; if (serverConfig.kvSecretEngineVersion == 1) { - return vaultClient.getSecretV1(clientToken, mount, path).data; + VaultKvSecretV1 secretV1 = vaultClient.getSecretV1(clientToken, mount, path); + return secretV1.data; } 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 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); + } + } } diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java index 5a00a8419..b10cb2937 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java @@ -6,6 +6,7 @@ import static io.quarkus.vault.runtime.client.OkHttpClientFactory.createHttpClie import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Map; 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.kv.VaultKvSecretV1; 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.VaultLeasesLookup; 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); } + @Override + public void writeSecretV1(String token, String secretEnginePath, String path, Map 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 public VaultRenewSelf renewSelf(String token, String increment) { VaultRenewSelfBody body = new VaultRenewSelfBody(increment); @@ -146,6 +169,11 @@ public class OkHttpVaultClient implements VaultClient { // --- + protected T delete(String path, String token, Object body, Class resultClass, int expectedCode) { + Request request = builder(path, token).delete(requestBody(body)).build(); + return exec(request, resultClass, expectedCode); + } + protected T post(String path, String token, Object body, Class resultClass, int expectedCode) { Request request = builder(path, token).post(requestBody(body)).build(); return exec(request, resultClass, expectedCode); diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java index a99f5891a..f7b20c363 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java @@ -1,5 +1,7 @@ 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.VaultKubernetesAuth; 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.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.client.dto.sys.VaultLeasesLookup; import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecrypt; @@ -43,6 +46,14 @@ public interface VaultClient { VaultKvSecretV2 getSecretV2(String token, String secretEnginePath, String path); + void writeSecretV1(String token, String secretEnginePath, String path, Map 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); VaultTransitEncrypt encrypt(String token, String keyName, VaultTransitEncryptBody body); diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2Write.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2Write.java new file mode 100644 index 000000000..1c7ab0490 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2Write.java @@ -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 { + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2WriteBody.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2WriteBody.java new file mode 100644 index 000000000..77ad1a53a --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2WriteBody.java @@ -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 options; + public Map data; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2WriteData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2WriteData.java new file mode 100644 index 000000000..b9f04bdb4 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/kv/VaultKvSecretV2WriteData.java @@ -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; +} diff --git a/integration-tests/vault-agroal/src/test/java/io/quarkus/vault/VaultKv2ITCase.java b/integration-tests/vault-agroal/src/test/java/io/quarkus/vault/VaultKv2ITCase.java new file mode 100644 index 000000000..be68cd873 --- /dev/null +++ b/integration-tests/vault-agroal/src/test/java/io/quarkus/vault/VaultKv2ITCase.java @@ -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); + } +} diff --git a/integration-tests/vault-app/src/main/java/io/quarkus/it/vault/VaultTestService.java b/integration-tests/vault-app/src/main/java/io/quarkus/it/vault/VaultTestService.java index 16a1b64d1..9cf471069 100644 --- a/integration-tests/vault-app/src/main/java/io/quarkus/it/vault/VaultTestService.java +++ b/integration-tests/vault-app/src/main/java/io/quarkus/it/vault/VaultTestService.java @@ -16,6 +16,7 @@ import org.jboss.logging.Logger; import io.quarkus.vault.VaultKVSecretEngine; import io.quarkus.vault.VaultTransitSecretEngine; +import io.quarkus.vault.runtime.client.VaultClientException; import io.quarkus.vault.transit.ClearData; import io.quarkus.vault.transit.SigningInput; @@ -48,12 +49,29 @@ public class VaultTestService { return "password=" + password + "; expected: " + expectedPassword; } + // basic Map secrets = kv.readSecret("foo"); String expectedSecrets = "{secret=s\u20accr\u20act}"; if (!expectedSecrets.equals(secrets.toString())) { 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 { List gifts = entityManager.createQuery("select g from Gift g").getResultList(); int count = gifts.size(); diff --git a/integration-tests/vault/src/test/java/io/quarkus/vault/VaultITCase.java b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultITCase.java index 3b7b38948..f15195e28 100644 --- a/integration-tests/vault/src/test/java/io/quarkus/vault/VaultITCase.java +++ b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultITCase.java @@ -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.VaultTransitVerifyBody; import io.quarkus.vault.runtime.config.VaultAuthenticationType; +import io.quarkus.vault.test.VaultTestExtension; import io.quarkus.vault.test.VaultTestLifecycleManager; import io.quarkus.vault.test.client.TestVaultClient; 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 CRUD_PATH = "crud"; + @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) @@ -130,6 +133,11 @@ public class VaultITCase { assertEquals("{" + SECRET_KEY + "=" + SECRET_VALUE + "}", secrets.toString()); } + @Test + public void crudSecretV1() { + VaultTestExtension.assertCrudSecret(kvSecretEngine); + } + @Test public void httpclient() { diff --git a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java index c5c4249cc..a4d56803c 100644 --- a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java +++ b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java @@ -20,7 +20,10 @@ import java.sql.Statement; import java.time.Duration; import java.time.Instant; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import java.util.TreeMap; import javax.sql.DataSource; @@ -32,6 +35,7 @@ import org.testcontainers.containers.Network; import org.testcontainers.containers.PostgreSQLContainer; import io.quarkus.vault.VaultException; +import io.quarkus.vault.VaultKVSecretEngine; import io.quarkus.vault.runtime.VaultManager; import io.quarkus.vault.runtime.client.VaultClientException; 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 TEST_QUERY_STRING = "SELECT 1"; + private static String CRUD_PATH = "crud"; + public GenericContainer vaultContainer; public PostgreSQLContainer postgresContainer; public String rootToken = null; @@ -98,6 +104,40 @@ public class VaultTestExtension { } } + public static void assertCrudSecret(VaultKVSecretEngine kvSecretEngine) { + + assertDeleteSecret(kvSecretEngine); + + assertDeleteSecret(kvSecretEngine); + + Map 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 secret = kvSecretEngine.readSecret(path); + return new TreeMap<>(secret).toString(); + } + private static VaultManager createVaultManager() { VaultRuntimeConfig serverConfig = new VaultRuntimeConfig(); serverConfig.tls = new VaultTlsConfig(); diff --git a/test-framework/vault/src/main/resources/vault.policy b/test-framework/vault/src/main/resources/vault.policy index c96e5894c..9a3d750b2 100644 --- a/test-framework/vault/src/main/resources/vault.policy +++ b/test-framework/vault/src/main/resources/vault.policy @@ -40,4 +40,12 @@ path "transit/*" { #} #path "transit/sign/my-sign-key" { # capabilities = [ "read", "update" ] -#} \ No newline at end of file +#} + +path "secret/crud" { + capabilities = ["read", "create", "update", "delete"] +} + +path "secret-v2/data/crud" { + capabilities = ["read", "create", "update", "delete"] +} \ No newline at end of file