Add Support to Multiple Vault KV Paths - Fixes Issue 5638 #6174

This commit is contained in:
Vincent Sevel
2019-11-25 23:03:08 +01:00
parent 9df9a09a24
commit 6bdff6ffde
6 changed files with 160 additions and 15 deletions

View File

@@ -14,14 +14,18 @@ import static io.quarkus.vault.runtime.config.VaultRuntimeConfig.DEFAULT_TLS_USE
import static io.quarkus.vault.runtime.config.VaultRuntimeConfig.KV_SECRET_ENGINE_VERSION_V1;
import static java.lang.Boolean.parseBoolean;
import static java.lang.Integer.parseInt;
import static java.util.Collections.emptyMap;
import static java.util.Optional.empty;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.AbstractMap.SimpleEntry;
import java.util.Collections;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -48,6 +52,8 @@ public class VaultConfigSource implements ConfigSource {
private static final String PROPERTY_PREFIX = "quarkus.vault.";
public static final Pattern CREDENTIAL_PATTERN = Pattern.compile("^quarkus\\.vault\\.credentials-provider\\.([^.]+)\\.");
public static final Pattern SECRET_CONFIG_KV_PATH_PATTERN = Pattern
.compile("^quarkus\\.vault\\.secret-config-kv-path\\.([^.]+)$");
private AtomicReference<VaultCacheEntry<Map<String, String>>> cache = new AtomicReference<>(null);
private AtomicReference<VaultRuntimeConfig> serverConfig = new AtomicReference<>(null);
@@ -76,7 +82,7 @@ public class VaultConfigSource implements ConfigSource {
*/
@Override
public Map<String, String> getProperties() {
return Collections.emptyMap();
return emptyMap();
}
@Override
@@ -102,13 +108,19 @@ public class VaultConfigSource implements ConfigSource {
Map<String, String> properties = new HashMap<>();
if (serverConfig.secretConfigKvPath.isPresent()) {
try {
properties.putAll(fetchSecrets(serverConfig));
log.debug("loaded " + properties.size() + " properties from vault");
} catch (RuntimeException e) {
return tryReturnLastKnownValue(e, cacheEntry);
try {
// default kv paths
if (serverConfig.secretConfigKvPath.isPresent()) {
fetchSecrets(serverConfig.secretConfigKvPath.get(), null, properties);
}
// prefixed kv paths
serverConfig.secretConfigKvPrefixPath.entrySet()
.forEach(entry -> fetchSecrets(entry.getValue(), entry.getKey(), properties));
log.debug("loaded " + properties.size() + " properties from vault");
} catch (RuntimeException e) {
return tryReturnLastKnownValue(e, cacheEntry);
}
cache.set(new VaultCacheEntry(properties));
@@ -116,11 +128,19 @@ public class VaultConfigSource implements ConfigSource {
}
private Map<String, String> fetchSecrets(VaultRuntimeConfig serverConfig) {
private void fetchSecrets(List<String> paths, String prefix, Map<String, String> properties) {
paths.forEach(path -> properties.putAll(fetchSecrets(path, prefix)));
}
private Map<String, String> fetchSecrets(String path, String prefix) {
VaultManager instance = getVaultManager();
return instance == null
? Collections.emptyMap()
: instance.getVaultKvManager().readSecret(serverConfig.secretConfigKvPath.get());
return instance == null ? emptyMap() : prefixMap(instance.getVaultKvManager().readSecret(path), prefix);
}
private Map<String, String> prefixMap(Map<String, String> map, String prefix) {
return prefix == null
? map
: map.entrySet().stream().collect(toMap(entry -> prefix + "." + entry.getKey(), Map.Entry::getValue));
}
// ---
@@ -176,7 +196,7 @@ public class VaultConfigSource implements ConfigSource {
getVaultProperty("kv-secret-engine-version", KV_SECRET_ENGINE_VERSION_V1));
serverConfig.kvSecretEngineMountPath = getVaultProperty("kv-secret-engine-mount-path",
DEFAULT_KV_SECRET_ENGINE_MOUNT_PATH);
serverConfig.secretConfigKvPath = getOptionalVaultProperty("secret-config-kv-path");
serverConfig.secretConfigKvPath = getOptionalListProperty("secret-config-kv-path");
serverConfig.tls.skipVerify = parseBoolean(getVaultProperty("tls.skip-verify", DEFAULT_TLS_SKIP_VERIFY));
serverConfig.tls.useKubernetesCaCert = parseBoolean(
getVaultProperty("tls.use-kubernetes-ca-cert", DEFAULT_TLS_USE_KUBERNETES_CACERT));
@@ -185,10 +205,25 @@ public class VaultConfigSource implements ConfigSource {
serverConfig.readTimeout = getVaultDuration("read-timeout", DEFAULT_READ_TIMEOUT);
serverConfig.credentialsProvider = getCredentialsProviders();
serverConfig.secretConfigKvPrefixPath = getSecretConfigKvPrefixPaths();
return serverConfig;
}
private Optional<List<String>> getOptionalListProperty(String name) {
Optional<String> optionalVaultProperty = getOptionalVaultProperty(name);
if (!optionalVaultProperty.isPresent()) {
return empty();
}
String[] split = optionalVaultProperty.get().split(",");
return Optional.of(Arrays.stream(split)
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(toList()));
}
private Optional<URL> newURL(Optional<String> url) {
try {
return Optional.ofNullable(url.isPresent() ? new URL(url.get()) : null);
@@ -232,6 +267,17 @@ public class VaultConfigSource implements ConfigSource {
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
}
private Map<String, List<String>> getSecretConfigKvPrefixPaths() {
return getConfigSourceStream()
.flatMap(configSource -> configSource.getPropertyNames().stream())
.map(this::getSecretConfigKvPrefixPathName)
.filter(Objects::nonNull)
.distinct()
.map(this::createNameSecretConfigKvPrefixPathPair)
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
}
private Stream<ConfigSource> getConfigSourceStream() {
Config config = ConfigProviderResolver.instance().getConfig();
return StreamSupport.stream(config.getConfigSources().spliterator(), false).filter(this::retain);
@@ -252,11 +298,20 @@ public class VaultConfigSource implements ConfigSource {
return new SimpleEntry<>(name, getCredentialsProviderConfig(name));
}
private SimpleEntry<String, List<String>> createNameSecretConfigKvPrefixPathPair(String name) {
return new SimpleEntry<>(name, getSecretConfigKvPrefixPath(name));
}
private String getCredentialsProviderName(String propertyName) {
Matcher matcher = CREDENTIAL_PATTERN.matcher(propertyName);
return matcher.find() ? matcher.group(1) : null;
}
private String getSecretConfigKvPrefixPathName(String propertyName) {
Matcher matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher(propertyName);
return matcher.find() ? matcher.group(1) : null;
}
private CredentialsProviderConfig getCredentialsProviderConfig(String name) {
String prefix = "credentials-provider." + name;
CredentialsProviderConfig config = new CredentialsProviderConfig();
@@ -266,4 +321,8 @@ public class VaultConfigSource implements ConfigSource {
return config;
}
private List<String> getSecretConfigKvPrefixPath(String prefixName) {
return getOptionalListProperty("secret-config-kv-path." + prefixName).get();
}
}

View File

@@ -8,6 +8,7 @@ import static io.quarkus.vault.runtime.config.VaultAuthenticationType.USERPASS;
import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -82,10 +83,35 @@ public class VaultRuntimeConfig {
public Duration secretConfigCachePeriod;
/**
* Vault path in kv store, where all properties will be available as MP config.
* List of comma separated vault paths in kv store,
* where all properties will be available as MP config properties as-is, with no prefix.
* For instance, if vault contains property {@code foo}, it will be made available to the
* quarkus application as
* {@code @ConfigProperty(name = "foo") String foo;}
* <p>
* If 2 paths contain the same property, the last path will win. For instance if
* {@code secret/base-config} contains {@code foo='bar'} and
* {@code secret/myapp/config} contains {@code foo='myappbar'}, then
* {@code @ConfigProperty(name = "foo") String foo;} will have for value {@code "bar"}
* with application properties
* {@code quarkus.vault.secret-config-kv-path=base-config,myapp/config}
*/
@ConfigItem
public Optional<String> secretConfigKvPath;
public Optional<List<String>> secretConfigKvPath;
/**
* List of comma separated vault paths in kv store,
* where all properties will be available as prefixed MP config properties.
* For instance if the application properties contains
* {@code quarkus.vault.secret-config-kv-path-prefix.myprefix=config}, all properties located
* in vault path {@code secret/config} contains {@code foo='bar'}, then {@code myprefix.foo}
* will be available in the MP config.
* <p>
* If the same property is available in 2 different paths for the same prefix, the last one
* will win.
*/
@ConfigItem(name = "secret-config-kv-path")
public Map<String, List<String>> secretConfigKvPrefixPath;
/**
* Used to hide confidential infos, for logging in particular.

View File

@@ -0,0 +1,42 @@
package io.quarkus.vault;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
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.VaultTestLifecycleManager;
@DisabledOnOs(OS.WINDOWS)
@QuarkusTestResource(VaultTestLifecycleManager.class)
public class VaultMultiPathConfigITCase {
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource("application-vault-multi-path.properties", "application.properties"));
@Test
public void defaultPath() {
Config config = ConfigProviderResolver.instance().getConfig();
assertEquals("red", config.getValue("color", String.class));
assertEquals("XL", config.getValue("size", String.class));
assertEquals("3", config.getValue("weight", String.class));
}
@Test
public void prefixPath() {
Config config = ConfigProviderResolver.instance().getConfig();
assertEquals("green", config.getValue("singer.color", String.class));
assertEquals("paul", config.getValue("singer.firstname", String.class));
assertEquals("simon", config.getValue("singer.lastname", String.class));
assertEquals("78", config.getValue("singer.age", String.class));
}
}

View File

@@ -0,0 +1,7 @@
quarkus.vault.url=https://localhost:8200
quarkus.vault.authentication.userpass.username=bob
quarkus.vault.authentication.userpass.password=sinclair
quarkus.vault.secret-config-kv-path=multi/default1,multi/default2
quarkus.vault.secret-config-kv-path.singer=multi/singer1,multi/singer2
quarkus.vault.tls.skip-verify=true

View File

@@ -204,6 +204,12 @@ public class VaultTestExtension {
execVault(format("vault kv put %s/%s %s=%s", SECRET_PATH_V1, APP_SECRET_PATH, SECRET_KEY, SECRET_VALUE));
execVault(format("vault kv put %s/%s %s=%s", SECRET_PATH_V1, APP_CONFIG_PATH, PASSWORD_PROPERTY_NAME, DB_PASSWORD));
// multi config
execVault(format("vault kv put %s/multi/default1 color=blue size=XL", SECRET_PATH_V1));
execVault(format("vault kv put %s/multi/default2 color=red weight=3", SECRET_PATH_V1));
execVault(format("vault kv put %s/multi/singer1 firstname=paul lastname=shaffer", SECRET_PATH_V1));
execVault(format("vault kv put %s/multi/singer2 lastname=simon age=78 color=green", SECRET_PATH_V1));
// static secrets kv v2
execVault(format("vault secrets enable -path=%s -version=2 kv", SECRET_PATH_V2));
execVault(format("vault kv put %s/%s %s=%s", SECRET_PATH_V2, APP_SECRET_PATH, SECRET_KEY, SECRET_VALUE));

View File

@@ -8,6 +8,11 @@ path "secret/config" {
capabilities = ["read"]
}
# vault config source kv engine v1 with multi paths
path "secret/multi/*" {
capabilities = ["read"]
}
# kv engine v2
path "secret-v2/data/foo" {
capabilities = ["read"]