Repository navigation
CAMEL-26919: camel-hashicorp-vault-starter - the early resolution logs in with the camel.vault.hashicorp options #2091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,17 +17,25 @@ | |
| package org.apache.camel.component.hashicorp.vault.springboot; | ||
|
|
||
| import org.apache.camel.RuntimeCamelException; | ||
| import org.apache.camel.component.hashicorp.vault.HashicorpVaultClient; | ||
| import org.apache.camel.component.hashicorp.vault.HashicorpVaultClientSettings; | ||
| import org.apache.camel.component.hashicorp.vault.HashicorpVaultPropertiesFunction; | ||
| import org.apache.camel.spi.PropertiesFunction; | ||
| import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser; | ||
| import org.apache.camel.util.ObjectHelper; | ||
| import org.apache.camel.vault.HashicorpVaultConfiguration; | ||
| import org.springframework.boot.context.properties.bind.Binder; | ||
| import org.springframework.core.env.ConfigurableEnvironment; | ||
| import org.springframework.vault.authentication.TokenAuthentication; | ||
| import org.springframework.vault.client.VaultEndpoint; | ||
| import org.springframework.vault.core.VaultTemplate; | ||
|
|
||
| /** | ||
| * Resolves the {@code {{hashicorp:...}}} placeholders of the Spring Boot properties before the CamelContext exists. It | ||
| * logs in to HashiCorp Vault with the {@code camel.vault.hashicorp} options, as the properties function of the | ||
| * CamelContext does, and revokes the token of its login once the properties are resolved. | ||
| */ | ||
| public class SpringBootHashicorpVaultPropertiesParser extends AbstractEarlyResolutionPropertiesParser { | ||
|
|
||
| private static final String PREFIX = "camel.vault.hashicorp"; | ||
|
|
||
| @Override | ||
| protected String getEarlyResolutionProperty() { | ||
| return "camel.component.hashicorp-vault.early-resolve-properties"; | ||
|
|
@@ -40,35 +48,62 @@ | |
|
|
||
| @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)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| 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)); | ||
|
Check failure on line 70 in components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The early resolution and the CamelContext can use different settings. This uses
Either use Claude Code (Opus 5.5) on behalf of Federico Mariani
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 Claude Code (Opus 5.5) on behalf of Federico Mariani
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the token login now sends an extra Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| } catch (IllegalArgumentException e) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: only Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| throw new RuntimeCamelException( | ||
| "Cannot create the Hashicorp Vault client of the " + PREFIX + " options: " + e.getMessage(), e); | ||
| } | ||
| return new EarlyResolutionFunction(client); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A failed login is retried once per placeholder. With a stale AppRole secret-id and 6 Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| } | ||
|
|
||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion for apache/camel#29819 rather than this PR. The private subclass and the Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
|
|
||
| private final HashicorpVaultClient client; | ||
|
|
||
| private EarlyResolutionFunction(HashicorpVaultClient client) { | ||
| super(client.getVaultTemplate()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HCP Vault: the secrets are read outside the namespace. So with Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| this.client = client; | ||
| } | ||
| return value; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possible flake: if a token is revoked between Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| .getRequiredData().get("path"))) | ||
| .toList(); | ||
| } | ||
|
|
||
| @Configuration | ||
| @AutoConfigureBefore(CamelAutoConfiguration.class) | ||
| static class TestConfiguration { | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional, fine as a follow-up JIRA: only the hashicorp parser overrides Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| } | ||
|
|
||
| /** | ||
| * Wording used in diagnostics to name where a value was being resolved from. | ||
| */ | ||
|
|
@@ -89,12 +96,20 @@ public final void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) | |
| return; | ||
| } | ||
|
|
||
| PropertiesFunction propertiesFunction = createPropertiesFunction(environment); | ||
| try { | ||
| resolve(environment, propertiesFunction); | ||
| } finally { | ||
| release(propertiesFunction); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Claude Code (Opus 5.5) on behalf of Federico Mariani |
||
| } | ||
| } | ||
|
|
||
| private void resolve(ConfigurableEnvironment environment, PropertiesFunction propertiesFunction) { | ||
| // an unresolved placeholder would otherwise stay in the property value and become the effective | ||
| // secret, so resolution failures abort startup unless the operator opts back into the old behaviour | ||
| final boolean ignoreResolutionFailures | ||
| = Boolean.parseBoolean(environment.getProperty(IGNORE_RESOLUTION_FAILURES)); | ||
|
|
||
| PropertiesFunction propertiesFunction = createPropertiesFunction(environment); | ||
| LOG.debug("Early resolving properties using the {} function", propertiesFunction.getName()); | ||
|
|
||
| final String prefix = "{{" + propertiesFunction.getName() + ":"; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unresolvable
${...}no longer fails fast. Before,environment.getPropertythrew "Could not resolve placeholder" forcamel.vault.hashicorp.token=${VAULT_TOKEN}withVAULT_TOKENunset.Binder.get(environment)usesPropertySourcesPlaceholdersResolver, which ignores unresolvable placeholders and keeps the literal text. Sorequired()passes and${VAULT_TOKEN}is sent asX-Vault-Token(or used as the host), which gives a vague 403 / URI error for each placeholder. Withcamel.vault.ignore-resolution-failures=truestartup 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