Croway commented on code in PR #2091:
URL:
https://github.com/apache/camel-spring-boot/pull/2091#discussion_r4563530590
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
+ required(configuration.getToken(), "token", "Hashicorp Vault token
is required");
+ }
+ required(configuration.getHost(), "host", "Hashicorp Vault host is
required");
+ required(configuration.getPort(), "port", "Hashicorp Vault port is
required");
+ required(configuration.getScheme(), "scheme", "Hashicorp Vault scheme
is required");
try {
- port = Integer.parseInt(portValue);
+ Integer.parseInt(configuration.getPort());
} catch (NumberFormatException e) {
throw new RuntimeCamelException(
- "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ PREFIX + ".port must be a number but was: " +
configuration.getPort(), e);
}
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ HashicorpVaultClient client;
+ try {
+ client =
HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeCamelException(
+ "Cannot create the Hashicorp Vault client of the " +
PREFIX + " options: " + e.getMessage(), e);
+ }
+ return new EarlyResolutionFunction(client);
+ }
- VaultTemplate client = new VaultTemplate(
- vaultEndpoint,
- new TokenAuthentication(token));
- return new HashicorpVaultPropertiesFunction(client);
+ @Override
+ protected void release(PropertiesFunction propertiesFunction) {
+ if (propertiesFunction instanceof EarlyResolutionFunction function) {
+ // stops renewing the token, and revokes the token of the login
(but not a token given for the token login)
+ function.client.close();
+ }
+ }
+
+ private static boolean isTokenLogin(HashicorpVaultConfiguration
configuration) {
+ String authentication = configuration.getAuthentication();
+ return ObjectHelper.isEmpty(authentication) ||
"token".equalsIgnoreCase(authentication.trim());
}
- private static String required(ConfigurableEnvironment environment, String
key, String message) {
- String value = environment.getProperty(key);
+ private static void required(String value, String option, String message) {
if (ObjectHelper.isEmpty(value)) {
- throw new RuntimeCamelException(message + " (set " + key + ")");
+ throw new RuntimeCamelException(message + " (set " + PREFIX + "."
+ option + ")");
+ }
+ }
+
+ /**
+ * The properties function of the early resolution, with the client it
reads the secrets with.
+ */
+ private static final class EarlyResolutionFunction extends
HashicorpVaultPropertiesFunction {
+
+ private final HashicorpVaultClient client;
+
+ private EarlyResolutionFunction(HashicorpVaultClient client) {
+ super(client.getVaultTemplate());
Review Comment:
**HCP Vault: the secrets are read outside the namespace.**
`super(client.getVaultTemplate())` uses the public
`HashicorpVaultPropertiesFunction(VaultTemplate)` constructor, which leaves
`cloud=false` and `namespace=null`. `HashicorpVaultClient` sends the login to
the namespace (its `loginClient`), but `apply()` adds the `namespace/` prefix
to the secret path only when `cloud` and `namespace` are set.
So with `camel.vault.hashicorp.cloud=true`, `namespace=admin` and
`authentication=approle`, the early resolution reads `secret/data/key` instead
of `admin/secret/data/key`, gets a 403 and aborts startup, while the
CamelContext reads the right path. The constructor that takes `cloud` and
`namespace` is package-private in apache/camel#29819, so the starter can't pass
them (see the comment on line 100).
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
+ required(configuration.getToken(), "token", "Hashicorp Vault token
is required");
+ }
+ required(configuration.getHost(), "host", "Hashicorp Vault host is
required");
+ required(configuration.getPort(), "port", "Hashicorp Vault port is
required");
+ required(configuration.getScheme(), "scheme", "Hashicorp Vault scheme
is required");
try {
- port = Integer.parseInt(portValue);
+ Integer.parseInt(configuration.getPort());
} catch (NumberFormatException e) {
throw new RuntimeCamelException(
- "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ PREFIX + ".port must be a number but was: " +
configuration.getPort(), e);
}
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ HashicorpVaultClient client;
+ try {
+ client =
HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
Review Comment:
**AppRole/JWT logins now happen twice per startup.** The early resolution
logs in with the same credentials that the CamelContext function and the secret
refresh task use later. With `secret_id_num_uses=1` (common with
response-wrapped secret IDs), the early resolution uses up the secret ID, and
the later CamelContext login fails with "invalid secret id": startup looks
fine, then the `hashicorp:` lookups in routes fail. Worth at least a note in
the docs.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
+ required(configuration.getToken(), "token", "Hashicorp Vault token
is required");
+ }
+ required(configuration.getHost(), "host", "Hashicorp Vault host is
required");
+ required(configuration.getPort(), "port", "Hashicorp Vault port is
required");
+ required(configuration.getScheme(), "scheme", "Hashicorp Vault scheme
is required");
try {
- port = Integer.parseInt(portValue);
+ Integer.parseInt(configuration.getPort());
} catch (NumberFormatException e) {
throw new RuntimeCamelException(
- "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ PREFIX + ".port must be a number but was: " +
configuration.getPort(), e);
}
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ HashicorpVaultClient client;
+ try {
+ client =
HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeCamelException(
+ "Cannot create the Hashicorp Vault client of the " +
PREFIX + " options: " + e.getMessage(), e);
+ }
+ return new EarlyResolutionFunction(client);
Review Comment:
**A failed login is retried once per placeholder.** `create` doesn't log in;
the `LifecycleAwareSessionManager` logs in on the first read and keeps no token
when the login fails, so every `apply()` logs in again. The base class then
goes through all the placeholders to report them together.
With a stale AppRole secret-id and 6 `hashicorp:` placeholders, one startup
sends 6 failed `auth/approle/login` calls. Vault's user lockout covers AppRole
(default threshold 5, 15 minutes), so the role_id is locked for every pod that
shares it, the CamelContext login included, even after the secret-id is fixed.
The old token-only path never logged in. Stopping after the first login
failure, or logging in once in `createPropertiesFunction`, would avoid this.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
+ required(configuration.getToken(), "token", "Hashicorp Vault token
is required");
+ }
+ required(configuration.getHost(), "host", "Hashicorp Vault host is
required");
+ required(configuration.getPort(), "port", "Hashicorp Vault port is
required");
+ required(configuration.getScheme(), "scheme", "Hashicorp Vault scheme
is required");
try {
- port = Integer.parseInt(portValue);
+ Integer.parseInt(configuration.getPort());
} catch (NumberFormatException e) {
throw new RuntimeCamelException(
- "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ PREFIX + ".port must be a number but was: " +
configuration.getPort(), e);
}
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ HashicorpVaultClient client;
+ try {
+ client =
HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
Review Comment:
**The early resolution and the CamelContext can use different settings.**
This uses `HashicorpVaultClientSettings.fromConfiguration`, while
`HashicorpVaultPropertiesFunction` on the CamelContext uses
`fromEnvironmentOrConfiguration`, where the `CAMEL_HASHICORP_VAULT_*` variables
win as soon as one of them is set. The `fromConfiguration` Javadoc says it is
meant for Spring Boot, but on Spring Boot the CamelContext function still reads
those variables, so "as the properties function of the CamelContext does" in
the class Javadoc doesn't hold there:
- Only `CAMEL_HASHICORP_VAULT_HOST` / `CAMEL_HASHICORP_VAULT_TOKEN` set (as
the Camel docs describe): the CamelContext resolves `{{hashicorp:...}}`, the
early resolution fails with "Hashicorp Vault token is required".
- Both set: the early resolution logs in with `camel.vault.hashicorp.*` (say
AppRole), the CamelContext with the environment token.
Either use `fromEnvironmentOrConfiguration` here too, or document that the
early resolution reads only `camel.vault.hashicorp.*`.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
+ required(configuration.getToken(), "token", "Hashicorp Vault token
is required");
+ }
+ required(configuration.getHost(), "host", "Hashicorp Vault host is
required");
+ required(configuration.getPort(), "port", "Hashicorp Vault port is
required");
+ required(configuration.getScheme(), "scheme", "Hashicorp Vault scheme
is required");
try {
- port = Integer.parseInt(portValue);
+ Integer.parseInt(configuration.getPort());
} catch (NumberFormatException e) {
throw new RuntimeCamelException(
- "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ PREFIX + ".port must be a number but was: " +
configuration.getPort(), e);
}
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ HashicorpVaultClient client;
+ try {
+ client =
HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
Review Comment:
Minor: **the token login now sends an extra `auth/token/lookup-self`.**
Before, it used `new VaultTemplate(endpoint, new TokenAuthentication(token))`,
i.e. a `SimpleSessionManager`. The client now goes through
`LifecycleAwareSessionManager`, which does a token self-lookup by default. A
token created with `num_uses=N` for N early-resolved secrets now runs out one
read early (403 on the last), and a token whose policy lacks `lookup-self` logs
"Cannot enhance VaultToken to a LoginToken" at WARN on every startup. It's a
behaviour change for the token login, which the PR otherwise keeps as it was.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
Review Comment:
**Unresolvable `${...}` no longer fails fast.** Before,
`environment.getProperty` threw "Could not resolve placeholder" for
`camel.vault.hashicorp.token=${VAULT_TOKEN}` with `VAULT_TOKEN` unset.
`Binder.get(environment)` uses `PropertySourcesPlaceholdersResolver`, which
ignores unresolvable placeholders and keeps the literal text. So `required()`
passes and `${VAULT_TOKEN}` is sent as `X-Vault-Token` (or used as the host),
which gives a vague 403 / URI error for each placeholder. With
`camel.vault.ignore-resolution-failures=true` startup goes on with the
placeholders unresolved. Checking the bound values for a leftover `${` would
keep the old behaviour.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParser.java:
##########
@@ -75,6 +75,13 @@ public abstract class AbstractEarlyResolutionPropertiesParser
*/
protected abstract PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment);
+ /**
+ * Releases what the properties function holds, such as the login of its
client, once the properties are resolved,
+ * also when they could not be. Does nothing by default.
+ */
+ protected void release(PropertiesFunction propertiesFunction) {
Review Comment:
Optional, fine as a follow-up JIRA: only the hashicorp parser overrides
`release`. The AWS parser (`SecretsManagerClient`, with its HTTP connection
pool) and the GCP parser (`SecretManagerServiceClient`, with its gRPC channel
and threads) still never close the clients of the early resolution. With this
hook in place they could close them too, or the base class could close a
function that is `AutoCloseable`.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
+ required(configuration.getToken(), "token", "Hashicorp Vault token
is required");
+ }
+ required(configuration.getHost(), "host", "Hashicorp Vault host is
required");
+ required(configuration.getPort(), "port", "Hashicorp Vault port is
required");
+ required(configuration.getScheme(), "scheme", "Hashicorp Vault scheme
is required");
try {
- port = Integer.parseInt(portValue);
+ Integer.parseInt(configuration.getPort());
} catch (NumberFormatException e) {
throw new RuntimeCamelException(
- "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ PREFIX + ".port must be a number but was: " +
configuration.getPort(), e);
}
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ HashicorpVaultClient client;
+ try {
+ client =
HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeCamelException(
+ "Cannot create the Hashicorp Vault client of the " +
PREFIX + " options: " + e.getMessage(), e);
+ }
+ return new EarlyResolutionFunction(client);
+ }
- VaultTemplate client = new VaultTemplate(
- vaultEndpoint,
- new TokenAuthentication(token));
- return new HashicorpVaultPropertiesFunction(client);
+ @Override
+ protected void release(PropertiesFunction propertiesFunction) {
+ if (propertiesFunction instanceof EarlyResolutionFunction function) {
+ // stops renewing the token, and revokes the token of the login
(but not a token given for the token login)
+ function.client.close();
+ }
+ }
+
+ private static boolean isTokenLogin(HashicorpVaultConfiguration
configuration) {
+ String authentication = configuration.getAuthentication();
+ return ObjectHelper.isEmpty(authentication) ||
"token".equalsIgnoreCase(authentication.trim());
}
- private static String required(ConfigurableEnvironment environment, String
key, String message) {
- String value = environment.getProperty(key);
+ private static void required(String value, String option, String message) {
if (ObjectHelper.isEmpty(value)) {
- throw new RuntimeCamelException(message + " (set " + key + ")");
+ throw new RuntimeCamelException(message + " (set " + PREFIX + "."
+ option + ")");
+ }
+ }
+
+ /**
+ * The properties function of the early resolution, with the client it
reads the secrets with.
+ */
+ private static final class EarlyResolutionFunction extends
HashicorpVaultPropertiesFunction {
Review Comment:
**Suggestion for apache/camel#29819 rather than this PR.** The private
subclass and the `instanceof` check in `release()` work around
`HashicorpVaultPropertiesFunction` not being able to take and own a
`HashicorpVaultClient`. Because the subclass can pass only a `VaultTemplate`,
it also loses `cloud` and `namespace` (comment on line 105). A public
constructor or factory in camel#29819 that takes the client and its settings,
and closes the client when the function stops, would fix both, and other
runtimes wouldn't need to re-implement it.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/EarlyResolvedPropertiesAppRoleTest.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.hashicorp.vault.springboot;
+
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import
org.apache.camel.test.infra.hashicorp.vault.services.HashicorpServiceFactory;
+import
org.apache.camel.test.infra.hashicorp.vault.services.HashicorpVaultService;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultKeyValueOperations;
+import org.springframework.vault.core.VaultKeyValueOperationsSupport;
+import org.springframework.vault.core.VaultTemplate;
+import org.springframework.vault.support.VaultMount;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The early resolution logs in with the AppRole login of the {@code
camel.vault.hashicorp} options, and revokes the
+ * token of its login once the properties are resolved.
+ */
+@CamelSpringBootTest
+@DirtiesContext
+@SpringBootApplication
+@SpringBootTest(
+ classes = { EarlyResolvedPropertiesAppRoleTest.TestConfiguration.class
},
+ properties = {
+
"camel.component.hashicorp-vault.early-resolve-properties=true",
+
"early.resolved.property.approle={{hashicorp:secret:approle#string}}"
+ })
+class EarlyResolvedPropertiesAppRoleTest {
+
+ @RegisterExtension
+ static HashicorpVaultService service =
HashicorpServiceFactory.createService();
+
+ // the system properties are seen by the early resolution, and the JVM is
shared with the other tests
+ private static final List<String> OPTIONS = List.of(
+ "camel.vault.hashicorp.host", "camel.vault.hashicorp.port",
"camel.vault.hashicorp.scheme",
+ "camel.vault.hashicorp.token",
"camel.vault.hashicorp.authentication", "camel.vault.hashicorp.role-id",
+ "camel.vault.hashicorp.secret-id");
+
+ @BeforeAll
+ static void setup() {
+ VaultTemplate root = root();
+ VaultKeyValueOperations secrets
+ = root.opsForKeyValue("secret",
VaultKeyValueOperationsSupport.KeyValueBackend.versioned());
+ secrets.put("approle", Map.of("string", "from-approle"));
+ // read by application.properties
+ secrets.put("database/password", Map.of("string", "pazzword"));
+
+ root.write("sys/policies/acl/early-resolution",
+ Map.of("policy", "path \"secret/data/*\" { capabilities =
[\"read\"] }"));
+ if (!root.opsForSys().getAuthMounts().containsKey("approle/")) {
+ root.opsForSys().authMount("approle",
VaultMount.create("approle"));
+ }
+ root.write("auth/approle/role/camel", Map.of("token_policies",
"early-resolution"));
+ String roleId = (String)
root.read("auth/approle/role/camel/role-id").getRequiredData().get("role_id");
+ String secretId = (String)
root.write("auth/approle/role/camel/secret-id", Map.of())
+ .getRequiredData().get("secret_id");
+
+ // no token: the AppRole login gets one
+ OPTIONS.forEach(System::clearProperty);
+ System.setProperty("camel.vault.hashicorp.host", service.host());
+ System.setProperty("camel.vault.hashicorp.port",
String.valueOf(service.port()));
+ System.setProperty("camel.vault.hashicorp.scheme", "http");
+ // the kebab-case names of Spring Boot
+ System.setProperty("camel.vault.hashicorp.authentication", "approle");
+ System.setProperty("camel.vault.hashicorp.role-id", roleId);
+ System.setProperty("camel.vault.hashicorp.secret-id", secretId);
+ }
+
+ @AfterAll
+ static void clearOptions() {
+ OPTIONS.forEach(System::clearProperty);
+ }
+
+ @Value("${early.resolved.property}")
+ private String earlyResolvedProperty;
+
+ @Value("${early.resolved.property.approle}")
+ private String earlyResolvedPropertyAppRole;
+
+ @Test
+ void resolvesThePropertiesWithTheAppRoleLoginAndRevokesItsToken() {
+ assertEquals("pazzword", earlyResolvedProperty);
+ assertEquals("from-approle", earlyResolvedPropertyAppRole);
+
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() ->
assertTrue(appRoleTokens().isEmpty(),
+ "the token of the AppRole login must be revoked once the
properties are resolved"));
+ }
+
+ private static VaultTemplate root() {
+ VaultEndpoint endpoint = VaultEndpoint.create(service.host(),
service.port());
+ endpoint.setScheme("http");
+ return new VaultTemplate(endpoint, new
TokenAuthentication(service.token()));
+ }
+
+ // the accessors of the tokens that the AppRole login gave and Vault has
not revoked
+ private static List<String> appRoleTokens() {
+ VaultTemplate root = root();
+ return root.list("auth/token/accessors").stream()
+ .filter(accessor -> "auth/approle/login".equals(
+ root.write("auth/token/lookup-accessor",
Map.of("accessor", accessor))
Review Comment:
Possible flake: if a token is revoked between `list("auth/token/accessors")`
and `lookup-accessor`, Vault answers 400 "invalid accessor" and `write` throws
a `VaultException`. `untilAsserted` retries only on `AssertionError`, so the
await fails at once instead of polling again, and that window is exactly when
the release revokes the token. `.ignoreExceptions()` on the await, or skipping
accessors whose lookup fails, would make it stable.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
Review Comment:
Nit: `isTokenLogin`, the host/port/scheme/token checks and the
`Integer.parseInt` repeat what `HashicorpVaultClient.create` already does
through `HashicorpVaultClient.validate(settings)` and
`HashicorpVaultAuthentication.of`. If Camel changes the default login or how
`authentication` is normalised, `isTokenLogin` drifts silently. Calling
`validate` and mapping its `IllegalArgumentException` to the current messages
would keep one copy of the rules.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java:
##########
@@ -40,35 +48,62 @@ protected String getOverridePropertySourceName() {
@Override
protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
- String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
- String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
- String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
- String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
-
- int port;
+ // bound as Spring Boot binds the options of the CamelContext, such as
camel.vault.hashicorp.role-id
+ HashicorpVaultConfiguration configuration = Binder.get(environment)
+ .bind(PREFIX, HashicorpVaultConfiguration.class)
+ .orElseGet(HashicorpVaultConfiguration::new);
+ if (isTokenLogin(configuration)) {
+ required(configuration.getToken(), "token", "Hashicorp Vault token
is required");
+ }
+ required(configuration.getHost(), "host", "Hashicorp Vault host is
required");
+ required(configuration.getPort(), "port", "Hashicorp Vault port is
required");
+ required(configuration.getScheme(), "scheme", "Hashicorp Vault scheme
is required");
try {
- port = Integer.parseInt(portValue);
+ Integer.parseInt(configuration.getPort());
} catch (NumberFormatException e) {
throw new RuntimeCamelException(
- "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ PREFIX + ".port must be a number but was: " +
configuration.getPort(), e);
}
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ HashicorpVaultClient client;
+ try {
+ client =
HashicorpVaultClient.create(HashicorpVaultClientSettings.fromConfiguration(configuration));
+ } catch (IllegalArgumentException e) {
Review Comment:
Nit: only `IllegalArgumentException` gets the message that names the
`camel.vault.hashicorp` options. Other failures from `create`, such as an
`IllegalStateException` (or `NoClassDefFoundError`) when
`authentication=aws-iam` and a needed AWS SDK module is missing, abort startup
without that hint. Catching `RuntimeException` here would cover more cases.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
##########
core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParser.java:
##########
@@ -89,12 +96,20 @@ public final void
onApplicationEvent(ApplicationEnvironmentPreparedEvent event)
return;
}
+ PropertiesFunction propertiesFunction =
createPropertiesFunction(environment);
+ try {
+ resolve(environment, propertiesFunction);
+ } finally {
+ release(propertiesFunction);
Review Comment:
**`release` can hide the resolution error.** If `resolve` throws (for
example the aggregated "Failed to resolve N property placeholder(s)" error) and
`release` throws too, the exception from `release` replaces it and the list of
unresolved keys is lost. Now that `release` is a protected extension point,
catching and logging its exception, or adding it with `addSuppressed`, would
keep the original error.
_Claude Code (Opus 5.5) on behalf of Federico Mariani_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]