diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultConfigSource.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultConfigSource.java index b79e35062b24f..b82e6010fa934 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultConfigSource.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultConfigSource.java @@ -16,6 +16,7 @@ import static java.lang.Integer.parseInt; import static java.util.Collections.emptyMap; import static java.util.Optional.empty; +import static java.util.regex.Pattern.compile; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; @@ -52,12 +53,13 @@ public class VaultConfigSource implements ConfigSource { private static final Logger log = Logger.getLogger(VaultConfigSource.class); - private static final String PROPERTY_PREFIX = "quarkus.vault."; - public static final Pattern CREDENTIALS_PATTERN = Pattern.compile("^quarkus\\.vault\\.credentials-provider\\.([^.]+)\\."); - public static final Pattern TRANSIT_KEY_PATTERN = Pattern.compile("^quarkus\\.vault\\.transit.key\\.([^.]+)\\."); - public static final Pattern SECRET_CONFIG_KV_PATH_PATTERN = Pattern - .compile("^quarkus\\.vault\\.secret-config-kv-path\\.(?:([^.]+)|(?:\"([^\"]+)\"))$"); - public static final Pattern EXPANSION_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); + static final String PROPERTY_PREFIX = "quarkus.vault."; + public static final Pattern CREDENTIALS_PATTERN = compile("^quarkus\\.vault\\.credentials-provider\\.([^.]+)\\."); + public static final Pattern TRANSIT_KEY_PATTERN = compile("^quarkus\\.vault\\.transit.key\\.([^.]+)\\."); + public static final String SECRET_CONFIG_KV_PREFIX_PATHS = "secret-config-kv-path"; + public static final Pattern SECRET_CONFIG_KV_PREFIX_PATH_PATTERN = compile( + "^quarkus\\.vault\\." + SECRET_CONFIG_KV_PREFIX_PATHS + "\\.(?:([^.]+)|(?:\"([^\"]+)\"))$"); + public static final Pattern EXPANSION_PATTERN = compile("\\$\\{([^}]+)\\}"); private AtomicReference>> cache = new AtomicReference<>(null); private AtomicReference serverConfig = new AtomicReference<>(null); @@ -117,13 +119,10 @@ private Map getSecretConfig() { try { // default kv paths - if (serverConfig.secretConfigKvPath.isPresent()) { - fetchSecrets(serverConfig.secretConfigKvPath.get(), null, properties); - } + serverConfig.secretConfigKvPath.ifPresent(strings -> fetchSecrets(strings, null, properties)); // prefixed kv paths - serverConfig.secretConfigKvPrefixPath.entrySet() - .forEach(entry -> fetchSecrets(entry.getValue(), entry.getKey(), properties)); + serverConfig.secretConfigKvPathPrefix.forEach((key, value) -> fetchSecrets(value.paths, key, properties)); log.debug("loaded " + properties.size() + " properties from vault"); } catch (RuntimeException e) { @@ -252,7 +251,7 @@ private VaultRuntimeConfig loadRuntimeConfig() { serverConfig.credentialsProvider = createCredentialProviderConfigParser().getConfig(); serverConfig.transit.key = createTransitKeyConfigParser().getConfig(); - serverConfig.secretConfigKvPrefixPath = getSecretConfigKvPrefixPaths(); + serverConfig.secretConfigKvPathPrefix = getSecretConfigKvPrefixPaths(); return serverConfig; } @@ -362,7 +361,7 @@ protected String getBaseProperty(String propertyName, String defaultValue) { .orElse(defaultValue); } - private Map> getSecretConfigKvPrefixPaths() { + private Map getSecretConfigKvPrefixPaths() { return getConfigSourceStream() .flatMap(configSource -> configSource.getPropertyNames().stream()) @@ -370,7 +369,8 @@ private Map> getSecretConfigKvPrefixPaths() { .filter(Objects::nonNull) .distinct() .map(this::createNameSecretConfigKvPrefixPathPair) - .collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue)); + .collect(toMap(SimpleEntry::getKey, + kvStore -> new VaultRuntimeConfig.KvPathConfig(kvStore.getValue()))); } private Stream getConfigSourceStream() { @@ -394,12 +394,12 @@ private SimpleEntry> createNameSecretConfigKvPrefixPathPair } private String getSecretConfigKvPrefixPathName(String propertyName) { - Matcher matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher(propertyName); + Matcher matcher = SECRET_CONFIG_KV_PREFIX_PATH_PATTERN.matcher(propertyName); return matcher.matches() ? (matcher.group(1) != null ? matcher.group(1) : matcher.group(2)) : null; } private List getSecretConfigKvPrefixPath(String prefixName) { - return getOptionalListProperty("secret-config-kv-path." + prefixName).get(); + return getOptionalListProperty(SECRET_CONFIG_KV_PREFIX_PATHS + "." + prefixName).get(); } } diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultRuntimeConfig.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultRuntimeConfig.java index 130d42b25270a..f3d6015c2a1a0 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultRuntimeConfig.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/config/VaultRuntimeConfig.java @@ -8,11 +8,14 @@ import java.net.URL; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -23,7 +26,6 @@ public class VaultRuntimeConfig { public static final String DEFAULT_KUBERNETES_JWT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"; public static final String DEFAULT_KV_SECRET_ENGINE_MOUNT_PATH = "secret"; - public static final String KV_SECRET_ENGINE_VERSION_V1 = "1"; public static final String KV_SECRET_ENGINE_VERSION_V2 = "2"; public static final String DEFAULT_RENEW_GRACE_PERIOD = "1H"; public static final String DEFAULT_SECRET_CONFIG_CACHE_PERIOD = "10M"; @@ -35,9 +37,9 @@ public class VaultRuntimeConfig { /** * Vault server url. - *

+ * * Example: https://localhost:8200 - *

+ * * See also the documentation for the `kv-secret-engine-mount-path` property for some insights on how * the full Vault url gets built. * @@ -55,7 +57,7 @@ public class VaultRuntimeConfig { /** * Renew grace period duration. - *

+ * * This value if used to extend a lease before it expires its ttl, or recreate a new lease before the current * lease reaches its max_ttl. * By default Vault leaseDuration is equal to 7 days (ie: 168h or 604800s). @@ -75,7 +77,7 @@ public class VaultRuntimeConfig { /** * Vault config source cache period. - *

+ * * Properties fetched from vault as MP config will be kept in a cache, and will not be fetched from vault * again until the expiration of that period. * This property is ignored if `secret-config-kv-path` is not set. @@ -89,20 +91,20 @@ public class VaultRuntimeConfig { /** * 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 `foo`, it will be made available to the * quarkus application as `@ConfigProperty(name = "foo") String foo;` - *

+ * * If 2 paths contain the same property, the last path will win. - *

+ * * For instance if - *

+ * * * `secret/base-config` contains `foo=bar` and * * `secret/myapp/config` contains `foo=myappbar`, then - *

+ * * `@ConfigProperty(name = "foo") String foo` will have value `myappbar` * with application properties `quarkus.vault.secret-config-kv-path=base-config,myapp/config` - *

+ * * See also the documentation for the `kv-secret-engine-mount-path` property for some insights on how * the full Vault url gets built. * @@ -112,32 +114,17 @@ public class VaultRuntimeConfig { @ConfigItem public Optional> secretConfigKvPath; - // @formatter:off /** - * 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 - * `quarkus.vault.secret-config-kv-path.myprefix=config`, and - * vault path `secret/config` contains `foo=bar`, then `myprefix.foo` - * will be available in the MP config. - *

- * If the same property is available in 2 different paths for the same prefix, the last one - * will win. - *

- * See also the documentation for the `kv-secret-engine-mount-path` property for some insights on how - * the full Vault url gets built. - * - * @asciidoclet + * KV store paths configuration. */ - // @formatter:on - @ConfigItem(name = "secret-config-kv-path.\"prefix\"") - public Map> secretConfigKvPrefixPath; + @ConfigItem(name = "secret-config-kv-path") + @ConfigDocMapKey("prefix") + public Map secretConfigKvPathPrefix; /** * Used to hide confidential infos, for logging in particular. * Possible values are: - *

+ * * * low: display all secrets. * * medium: display only usernames and lease ids (ie: passwords and tokens are masked). * * high: hide lease ids and dynamic credentials username. @@ -149,7 +136,7 @@ public class VaultRuntimeConfig { /** * Kv secret engine version. - *

+ * * see https://www.vaultproject.io/docs/secrets/kv/index.html * * @asciidoclet @@ -159,24 +146,24 @@ public class VaultRuntimeConfig { /** * KV secret engine path. - *

+ * * This value is used when building the url path in the KV secret engine programmatic access * (i.e. `VaultKVSecretEngine`) and the vault config source (i.e. fetching configuration properties from Vault). - *

+ * * For a v2 KV secret engine (default - see `kv-secret-engine-version property`) * the full url is built from the expression `/v1//data/...`. - *

+ * * With property `quarkus.vault.url=https://localhost:8200`, the following call * `vaultKVSecretEngine.readSecret("foo/bar")` would lead eventually to a `GET` on Vault with the following * url: `https://localhost:8200/v1/secret/data/foo/bar`. - *

+ * * With a KV secret engine v1, the url changes to: `/v1//...`. - *

+ * * The same logic is applied to the Vault config source. With `quarkus.vault.secret-config-kv-path=config/myapp` * The secret properties would be fetched from Vault using a `GET` on * `https://localhost:8200/v1/secret/data/config/myapp` for a KV secret engine v2 (or * `https://localhost:8200/v1/secret/config/myapp` for a KV secret engine v1). - *

+ * * see https://www.vaultproject.io/docs/secrets/kv/index.html * * @asciidoclet @@ -205,10 +192,10 @@ public class VaultRuntimeConfig { /** * List of named credentials providers, such as: `quarkus.vault.credentials-provider.foo.kv-path=mypath` - *

+ * * This defines a credentials provider `foo` returning key `password` from vault path `mypath`. * Once defined, this provider can be used in credentials consumers, such as the Agroal connection pool. - *

+ * * Example: `quarkus.datasource.credentials-provider=foo` * * @asciidoclet @@ -271,4 +258,43 @@ public String toString() { '}'; } + @ConfigGroup + public static class KvPathConfig { + // @formatter:off + /** + * 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 + * `quarkus.vault.secret-config-kv-path.myprefix=config`, and + * vault path `secret/config` contains `foo=bar`, then `myprefix.foo` + * will be available in the MP config. + * + * If the same property is available in 2 different paths for the same prefix, the last one + * will win. + * + * See also the documentation for the `quarkus.vault.kv-secret-engine-mount-path` property for some insights on how + * the full Vault url gets built. + * + * @asciidoclet + */ + // @formatter:on + @ConfigItem(name = ConfigItem.PARENT) + List paths; + + public KvPathConfig(List paths) { + this.paths = paths; + } + + public KvPathConfig() { + paths = Collections.emptyList(); + } + + @Override + public String toString() { + return "SecretConfigKvPathConfig{" + + "paths=" + paths + + '}'; + } + } } diff --git a/extensions/vault/runtime/src/test/java/io/quarkus/vault/runtime/config/VaultConfigSourceTest.java b/extensions/vault/runtime/src/test/java/io/quarkus/vault/runtime/config/VaultConfigSourceTest.java index f4e8f5a2675b5..47ff8245c448f 100644 --- a/extensions/vault/runtime/src/test/java/io/quarkus/vault/runtime/config/VaultConfigSourceTest.java +++ b/extensions/vault/runtime/src/test/java/io/quarkus/vault/runtime/config/VaultConfigSourceTest.java @@ -1,6 +1,8 @@ package io.quarkus.vault.runtime.config; -import static io.quarkus.vault.runtime.config.VaultConfigSource.SECRET_CONFIG_KV_PATH_PATTERN; +import static io.quarkus.vault.runtime.config.VaultConfigSource.PROPERTY_PREFIX; +import static io.quarkus.vault.runtime.config.VaultConfigSource.SECRET_CONFIG_KV_PREFIX_PATHS; +import static io.quarkus.vault.runtime.config.VaultConfigSource.SECRET_CONFIG_KV_PREFIX_PATH_PATTERN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,16 +12,18 @@ public class VaultConfigSourceTest { + private static final String PROPERTY = PROPERTY_PREFIX + SECRET_CONFIG_KV_PREFIX_PATHS; + @Test void secretConfigKvPathPattern() { Matcher matcher; - matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher("quarkus.vault.secret-config-kv-path.hello"); + matcher = SECRET_CONFIG_KV_PREFIX_PATH_PATTERN.matcher(PROPERTY + ".hello"); assertTrue(matcher.matches()); assertEquals("hello", matcher.group(1)); - matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher("quarkus.vault.secret-config-kv-path.\"mp.jwt.verify\""); + matcher = SECRET_CONFIG_KV_PREFIX_PATH_PATTERN.matcher(PROPERTY + ".\"mp.jwt.verify\""); assertTrue(matcher.matches()); assertEquals("mp.jwt.verify", matcher.group(2)); }