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 8c4595c76fd54..557e002583523 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 @@ -49,6 +49,7 @@ public interface VaultClient { String X_VAULT_TOKEN = "X-Vault-Token"; + String X_VAULT_NAMESPACE = "X-Vault-Namespace"; String API_VERSION = "v1"; VaultUserPassAuth loginUserPass(String user, String password); diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VertxVaultClient.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VertxVaultClient.java index 9dc150f158113..02f9f581d5362 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VertxVaultClient.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VertxVaultClient.java @@ -7,8 +7,11 @@ import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.PreDestroy; import javax.inject.Singleton; @@ -88,6 +91,9 @@ public class VertxVaultClient implements VaultClient { private static final Logger log = Logger.getLogger(VertxVaultClient.class); + private static final List ROOT_NAMESPACE_API = Arrays.asList("sys/init", "sys/license", "sys/leader", "sys/health", + "sys/metrics", "sys/config/state", "sys/host-info", "sys/key-status", "sys/storage", "sys/storage/raft"); + private final Vertx vertx; private URL baseUrl; private String kubernetesAuthMountPath; @@ -508,9 +514,17 @@ private HttpRequest builder(String path, String token) { if (token != null) { request.putHeader(X_VAULT_TOKEN, token); } + Optional namespace = vaultConfigHolder.getVaultBootstrapConfig().enterprise.namespace; + if (namespace.isPresent() && !isRootNamespaceAPI(path)) { + request.putHeader(X_VAULT_NAMESPACE, namespace.get()); + } return request; } + private boolean isRootNamespaceAPI(String path) { + return ROOT_NAMESPACE_API.stream().anyMatch(path::startsWith); + } + private HttpRequest builder(String path) { return webClient.getAbs(getUrl(path).toString()); } diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java index 4d9787aa1dd39..5f82842529cff 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultBootstrapConfig.java @@ -61,6 +61,13 @@ public class VaultBootstrapConfig { @ConfigItem public Optional url; + /** + * Vault Enterprise + */ + @ConfigItem + @ConfigDocSection + public VaultEnterpriseConfig enterprise; + /** * Authentication */ diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultEnterpriseConfig.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultEnterpriseConfig.java new file mode 100644 index 0000000000000..6d575b948b707 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultEnterpriseConfig.java @@ -0,0 +1,22 @@ +package io.quarkus.vault.runtime.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class VaultEnterpriseConfig { + + /** + * Vault Enterprise namespace + *

+ * If set, this will add a `X-Vault-Namespace` header to all requests sent to the Vault server. + *

+ * See https://www.vaultproject.io/docs/enterprise/namespaces + * + * @asciidoclet + */ + @ConfigItem + public Optional namespace; +} diff --git a/integration-tests/vault/pom.xml b/integration-tests/vault/pom.xml index 334221e1f9b45..de3f996b7265f 100644 --- a/integration-tests/vault/pom.xml +++ b/integration-tests/vault/pom.xml @@ -23,6 +23,16 @@ io.quarkus quarkus-test-vault test + + + org.hamcrest + hamcrest-core + + + + + jakarta.servlet + jakarta.servlet-api io.quarkus @@ -34,6 +44,11 @@ assertj-core test + + com.github.tomakehurst + wiremock-jre8 + test + diff --git a/integration-tests/vault/src/test/java/io/quarkus/vault/VaultEnterpriseITCase.java b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultEnterpriseITCase.java new file mode 100644 index 0000000000000..a1d65d4e626f0 --- /dev/null +++ b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultEnterpriseITCase.java @@ -0,0 +1,33 @@ +package io.quarkus.vault; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +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; + +@DisabledOnOs(OS.WINDOWS) // https://github.com/quarkusio/quarkus/issues/3796 +@QuarkusTestResource(WiremockVault.class) +public class VaultEnterpriseITCase { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("application-vault-enterprise.properties", "application.properties")); + + @Inject + VaultKVSecretEngine kvSecretEngine; + + @Test + public void header() { + assertEquals("{hello=world}", kvSecretEngine.readSecret("foo").toString()); + } +} diff --git a/integration-tests/vault/src/test/java/io/quarkus/vault/WiremockVault.java b/integration-tests/vault/src/test/java/io/quarkus/vault/WiremockVault.java new file mode 100644 index 0000000000000..9c170d6c5830c --- /dev/null +++ b/integration-tests/vault/src/test/java/io/quarkus/vault/WiremockVault.java @@ -0,0 +1,43 @@ +package io.quarkus.vault; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +import java.util.Collections; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class WiremockVault implements QuarkusTestResourceLifecycleManager { + + private WireMockServer server; + + @Override + public Map start() { + + server = new WireMockServer(wireMockConfig().httpsPort(8201)); + server.start(); + + server.stubFor(get(urlEqualTo("/v1/secret/foo")) + .withHeader("X-Vault-Namespace", equalTo("accounting")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody( + "{\"request_id\":\"bf5245f4-f194-2b13-80b7-6cad145b8135\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"wrap_info\":null,\"warnings\":null,\"auth\":null," + + "\"data\":{\"hello\":\"world\"}}"))); + + return Collections.emptyMap(); + } + + @Override + public void stop() { + if (server != null) { + server.stop(); + } + } +} diff --git a/integration-tests/vault/src/test/resources/application-vault-enterprise.properties b/integration-tests/vault/src/test/resources/application-vault-enterprise.properties new file mode 100644 index 0000000000000..92fc085d54aa8 --- /dev/null +++ b/integration-tests/vault/src/test/resources/application-vault-enterprise.properties @@ -0,0 +1,7 @@ +quarkus.vault.url=https://localhost:8201 +quarkus.vault.enterprise.namespace=accounting +quarkus.vault.authentication.client-token=123 +quarkus.vault.kv-secret-engine-version=1 +quarkus.tls.trust-all=true +# CI can sometimes be slow, there is no need to fail a test if Vault doesn't respond in 1 second which is the default +quarkus.vault.read-timeout=5S 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 09321f800dfb3..f9272df69ef66 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 @@ -18,7 +18,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; -import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; @@ -55,6 +54,7 @@ import io.quarkus.vault.runtime.client.dto.sys.VaultSealStatusResult; import io.quarkus.vault.runtime.config.VaultAuthenticationConfig; import io.quarkus.vault.runtime.config.VaultBootstrapConfig; +import io.quarkus.vault.runtime.config.VaultEnterpriseConfig; import io.quarkus.vault.runtime.config.VaultKubernetesAuthenticationConfig; import io.quarkus.vault.runtime.config.VaultTlsConfig; import io.quarkus.vault.test.client.TestVaultClient; @@ -67,7 +67,6 @@ public class VaultTestExtension { static final String DB_USERNAME = "postgres"; public static final String DB_PASSWORD = "bar"; public static final String SECRET_VALUE = "s\u20accr\u20act"; - static final String DEFAULT_VAULT_VERSION = "1.6.0"; static final int VAULT_PORT = 8200; static final int MAPPED_POSTGRESQL_PORT = 6543; public static final String VAULT_AUTH_USERPASS_USER = "bob"; @@ -112,7 +111,7 @@ public class VaultTestExtension { public String passwordKvv2WrappingToken = null; public String anotherPasswordKvv2WrappingToken = null; - private TestVaultClient vaultClient = createVaultClient(); + private TestVaultClient vaultClient; private String db_default_ttl = "1m"; private String db_max_ttl = "10m"; @@ -162,10 +161,12 @@ private static String readSecretAsString(VaultKVSecretEngine kvSecretEngine, Str return new TreeMap<>(secret).toString(); } - private static TestVaultClient createVaultClient() { + private TestVaultClient createVaultClient() { VaultBootstrapConfig vaultBootstrapConfig = new VaultBootstrapConfig(); vaultBootstrapConfig.tls = new VaultTlsConfig(); vaultBootstrapConfig.url = getVaultUrl(); + vaultBootstrapConfig.enterprise = new VaultEnterpriseConfig(); + vaultBootstrapConfig.enterprise.namespace = Optional.empty(); vaultBootstrapConfig.tls.skipVerify = Optional.of(true); vaultBootstrapConfig.tls.caCert = Optional.empty(); vaultBootstrapConfig.connectTimeout = Duration.ofSeconds(5); @@ -183,7 +184,7 @@ private static Optional getVaultUrl() { } } - public void start() throws InterruptedException, IOException, URISyntaxException { + public void start() throws InterruptedException, IOException { log.info("start containers on " + System.getProperty("os.name")); @@ -205,11 +206,12 @@ public void start() throws InterruptedException, IOException, URISyntaxException String configFile = useTls() ? "vault-config-tls.json" : "vault-config.json"; - log.info("starting vault with url=" + VAULT_URL + " and config file=" + configFile); + String vaultImage = getVaultImage(); + log.info("starting " + vaultImage + " with url=" + VAULT_URL + " and config file=" + configFile); new File(HOST_VAULT_TMP_CMD).mkdirs(); - vaultContainer = new GenericContainer<>("vault:" + getVaultVersion()) + vaultContainer = new GenericContainer<>(vaultImage) .withExposedPorts(VAULT_PORT) .withEnv("SKIP_SETCAP", "true") .withEnv("VAULT_SKIP_VERIFY", "true") // this is internal to the container @@ -235,11 +237,13 @@ public void start() throws InterruptedException, IOException, URISyntaxException log.info("vault has started with root token: " + rootToken); } - private String getVaultVersion() { - return System.getProperty("vault.version", DEFAULT_VAULT_VERSION); + private String getVaultImage() { + return "vault:1.6.0"; } - private void initVault() throws InterruptedException, IOException, URISyntaxException { + private void initVault() throws InterruptedException, IOException { + + vaultClient = createVaultClient(); VaultInitResponse vaultInit = vaultClient.init(1, 1); String unsealKey = vaultInit.keys.get(0); @@ -454,5 +458,4 @@ public void close() { // VaultManager.getInstance().reset(); } - } diff --git a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java index 4a8669a899dda..28b1d33f12a53 100644 --- a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java +++ b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestLifecycleManager.java @@ -1,7 +1,6 @@ package io.quarkus.vault.test; import java.io.IOException; -import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; @@ -13,25 +12,26 @@ public class VaultTestLifecycleManager implements QuarkusTestResourceLifecycleMa private static final Logger log = Logger.getLogger(VaultTestLifecycleManager.class); - private VaultTestExtension vaultTestExtension = new VaultTestExtension(); + protected VaultTestExtension vaultTestExtension; public static final String GRAALVM_JRE_LIB_AMD_64 = "/opt/graalvm/jre/lib/amd64"; @Override public Map start() { + vaultTestExtension = new VaultTestExtension(); + Map sysprops = new HashMap<>(); // see TLS availability in native mode https://github.com/quarkusio/quarkus/issues/3797 if (VaultTestExtension.useTls()) { - sysprops.put("quarkus.vault.url", "https://localhost:8200"); sysprops.put("javax.net.ssl.trustStore", "src/test/resources/vaultTrustStore"); sysprops.put("java.library.path", GRAALVM_JRE_LIB_AMD_64); } try { vaultTestExtension.start(); - } catch (InterruptedException | IOException | URISyntaxException e) { + } catch (InterruptedException | IOException e) { throw new RuntimeException(e); } @@ -51,6 +51,8 @@ public Map start() { @Override public void stop() { - vaultTestExtension.close(); + if (vaultTestExtension != null) { + vaultTestExtension.close(); + } } }