From f4c372e91c31a1bf7546659587cf421caa7ff5bc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 27 Aug 2026 19:32:40 +0000 Subject: [PATCH 1/3] CAMEL-24532: Preserve property-source precedence during early vault resolution Early-resolution listeners iterate Spring PropertySources highest-precedence first but used Properties.put, so duplicate keys were overwritten by lower- precedence sources before the flat override map was added with addFirst. Introduce EarlyResolutionPropertySources with putIfAbsent/asString helpers, update all vault and jasypt early-resolution parsers, and add regression tests. Co-authored-by: Cursor Agent --- ...BootAwsSecretsManagerPropertiesParser.java | 12 +-- ...ringBootAzureKeyVaultPropertiesParser.java | 12 +-- ...ringBootCyberArkVaultPropertiesParser.java | 12 +-- ...otGoogleSecretManagerPropertiesParser.java | 12 +-- ...ingBootHashicorpVaultPropertiesParser.java | 12 +-- ...BMSecretsManagerVaultPropertiesParser.java | 12 +-- .../SpringBootJasyptPropertiesParser.java | 13 ++-- ...SpringBootCloudConfigPropertiesParser.java | 12 +-- .../boot/EarlyResolutionPropertySources.java | 57 ++++++++++++++ .../EarlyResolutionPropertySourcesTest.java | 76 +++++++++++++++++++ 10 files changed, 161 insertions(+), 69 deletions(-) create mode 100644 core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java create mode 100644 core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java diff --git a/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java b/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java index 25b02114f798..c9fa0adc933b 100644 --- a/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java +++ b/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java @@ -18,11 +18,11 @@ import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.aws.secretsmanager.SecretsManagerPropertiesFunction; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -80,13 +80,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - String stringValue = null; - if ((value instanceof OriginTrackedValue originTrackedValue && - originTrackedValue.getValue() instanceof String v)) { - stringValue = v; - } else if (value instanceof String v) { - stringValue = v; - } + String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{aws:") && @@ -96,7 +90,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String element = secretsManagerPropertiesFunction.apply(stringValue .replace("{{aws:", "") .replace("}}", "")); - props.put(key, element); + EarlyResolutionPropertySources.putIfAbsent(props, key, element); } catch (Exception e) { if (ignoreResolutionFailures) { LOG.warn("Failed to resolve property {} from the vault; the placeholder is left " diff --git a/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java b/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java index f240c1cd0132..24c13997c304 100644 --- a/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java +++ b/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java @@ -24,11 +24,11 @@ import com.azure.security.keyvault.secrets.SecretClientBuilder; import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.azure.key.vault.KeyVaultPropertiesFunction; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -90,13 +90,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - String stringValue = null; - if ((value instanceof OriginTrackedValue originTrackedValue && - originTrackedValue.getValue() instanceof String v)) { - stringValue = v; - } else if (value instanceof String v) { - stringValue = v; - } + String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{azure:") && stringValue.endsWith("}}")) { @@ -105,7 +99,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String element = keyVaultPropertiesFunction.apply(stringValue .replace("{{azure:", "") .replace("}}", "")); - props.put(key, element); + EarlyResolutionPropertySources.putIfAbsent(props, key, element); } catch (Exception e) { if (ignoreResolutionFailures) { LOG.warn("Failed to resolve property {} from the vault; the placeholder is left " diff --git a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java index 4242da15ea29..84ae1b0fdfb5 100644 --- a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java +++ b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java @@ -18,13 +18,13 @@ import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.cyberark.vault.CyberArkVaultPropertiesFunction; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.component.cyberark.vault.client.ConjurClient; import org.apache.camel.component.cyberark.vault.client.ConjurClientFactory; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -78,13 +78,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - String stringValue = null; - if ((value instanceof OriginTrackedValue originTrackedValue && - originTrackedValue.getValue() instanceof String v)) { - stringValue = v; - } else if (value instanceof String v) { - stringValue = v; - } + String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{cyberark:") && @@ -94,7 +88,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String element = cyberArkVaultPropertiesFunction.apply(stringValue .replace("{{cyberark:", "") .replace("}}", "")); - props.put(key, element); + EarlyResolutionPropertySources.putIfAbsent(props, key, element); } catch (Exception e) { if (ignoreResolutionFailures) { LOG.warn("Failed to resolve property {} from the vault; the placeholder is left " diff --git a/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java b/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java index 696c9c4d0de6..9d2e778097be 100644 --- a/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java +++ b/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java @@ -20,11 +20,11 @@ import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.google.secret.manager.GoogleSecretManagerPropertiesFunction; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -66,13 +66,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - String stringValue = null; - if ((value instanceof OriginTrackedValue originTrackedValue && - originTrackedValue.getValue() instanceof String v)) { - stringValue = v; - } else if (value instanceof String v) { - stringValue = v; - } + String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{gcp:") && stringValue.endsWith("}}")) { @@ -81,7 +75,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String element = secretsManagerPropertiesFunction.apply(stringValue .replace("{{gcp:", "") .replace("}}", "")); - props.put(key, element); + EarlyResolutionPropertySources.putIfAbsent(props, key, element); } catch (Exception e) { if (ignoreResolutionFailures) { LOG.warn("Failed to resolve property {} from the vault; the placeholder is left " diff --git a/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java b/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java index eb930f40d1f0..ccce593c58b6 100644 --- a/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java +++ b/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java @@ -18,10 +18,10 @@ import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.hashicorp.vault.HashicorpVaultPropertiesFunction; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -70,20 +70,14 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - String stringValue = null; - if ((value instanceof OriginTrackedValue originTrackedValue && - originTrackedValue.getValue() instanceof String v)) { - stringValue = v; - } else if (value instanceof String v) { - stringValue = v; - } + String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{hashicorp:") && stringValue.endsWith("}}")) { LOG.debug("decrypting and overriding property {}", key); try { - props.put(key, hashicorpVaultPropertiesFunction.apply(stringValue + EarlyResolutionPropertySources.putIfAbsent(props, key, hashicorpVaultPropertiesFunction.apply(stringValue .replace("{{hashicorp:", "") .replace("}}", ""))); } catch (Exception e) { diff --git a/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java b/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java index 5bca3dc9ef86..ca0da174a492 100644 --- a/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java +++ b/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java @@ -20,11 +20,11 @@ import com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.SecretsManager; import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.ibm.secrets.manager.IBMSecretsManagerPropertiesFunction; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -64,13 +64,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - String stringValue = null; - if ((value instanceof OriginTrackedValue originTrackedValue && - originTrackedValue.getValue() instanceof String v)) { - stringValue = v; - } else if (value instanceof String v) { - stringValue = v; - } + String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{ibm:") && stringValue.endsWith("}}")) { @@ -79,7 +73,7 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String element = secretsManagerPropertiesFunction.apply(stringValue .replace("{{ibm:", "") .replace("}}", "")); - props.put(key, element); + EarlyResolutionPropertySources.putIfAbsent(props, key, element); } catch (Exception e) { if (ignoreResolutionFailures) { LOG.warn("Failed to resolve property {} from the vault; the placeholder is left " diff --git a/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java b/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java index b75bde48a512..6367aefb5b21 100644 --- a/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java +++ b/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java @@ -17,6 +17,7 @@ package org.apache.camel.component.jasypt.springboot; import org.apache.camel.component.jasypt.JasyptPropertiesParser; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.component.properties.PropertiesParser; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.StringHelper; @@ -25,7 +26,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -70,14 +70,15 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - if (value instanceof OriginTrackedValue originTrackedValue && - originTrackedValue.getValue() instanceof String stringValue && - stringValue.startsWith(JasyptPropertiesParser.JASYPT_PREFIX_TOKEN) && - stringValue.endsWith(JasyptPropertiesParser.JASYPT_SUFFIX_TOKEN)) { + String stringValue = EarlyResolutionPropertySources.asString(value); + if (stringValue != null + && stringValue.startsWith(JasyptPropertiesParser.JASYPT_PREFIX_TOKEN) + && stringValue.endsWith(JasyptPropertiesParser.JASYPT_SUFFIX_TOKEN)) { LOG.debug("decrypting and overriding property {}", key); try { - props.put(key, propertiesParser.parseProperty(key.toString(), stringValue, null)); + EarlyResolutionPropertySources.putIfAbsent(props, key, + propertiesParser.parseProperty(key.toString(), stringValue, null)); } catch (Exception e) { // Log and do nothing LOG.debug("failed to parse property {}", key, e); diff --git a/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java b/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java index d2c951b90238..9f34bec54892 100644 --- a/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java +++ b/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java @@ -18,10 +18,10 @@ import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.spring.cloud.config.SpringCloudConfigPropertiesFunction; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; -import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; @@ -49,20 +49,14 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { for (PropertySource mutablePropertySources : event.getEnvironment().getPropertySources()) { if (mutablePropertySources instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { - String stringValue = null; - if ((value instanceof OriginTrackedValue originTrackedValue - && originTrackedValue.getValue() instanceof String v)) { - stringValue = v; - } else if (value instanceof String v) { - stringValue = v; - } + String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{spring-config:") && stringValue.endsWith("}}")) { LOG.debug("decrypting and overriding property {}", key); try { String element = springCloudConfigPropertiesFunction .apply(stringValue.replace("{{spring-config:", "").replace("}}", "")); - properties.put(key, element); + EarlyResolutionPropertySources.putIfAbsent(properties, key, element); } catch (Exception e) { if (ignoreResolutionFailures) { LOG.warn("Failed to resolve property {} from Spring Cloud Config; the placeholder is left " diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java new file mode 100644 index 000000000000..bf7f1101cc31 --- /dev/null +++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java @@ -0,0 +1,57 @@ +/* + * 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.spring.boot; + +import java.util.Properties; + +import org.springframework.boot.origin.OriginTrackedValue; + +/** + * Helpers for early property resolution listeners that inject a flat {@link Properties} override via + * {@code PropertySources.addFirst}. + *

+ * Spring iterates property sources in precedence order (highest first). When the same key appears in multiple + * sources, callers must retain the first resolved value so the override map reflects Spring's normal precedence + * after it is added with {@code addFirst}. + */ +public final class EarlyResolutionPropertySources { + + private EarlyResolutionPropertySources() { + } + + /** + * Returns the string value from a property source entry, including {@link OriginTrackedValue} wrappers. + */ + public static String asString(Object value) { + if (value instanceof OriginTrackedValue originTrackedValue + && originTrackedValue.getValue() instanceof String stringValue) { + return stringValue; + } + if (value instanceof String stringValue) { + return stringValue; + } + return null; + } + + /** + * Stores a resolved value only when the key is not already present, preserving highest-precedence values + * collected while iterating property sources. + */ + public static void putIfAbsent(Properties props, Object key, String resolvedValue) { + props.putIfAbsent(key.toString(), resolvedValue); + } +} diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java new file mode 100644 index 000000000000..929eda7abdff --- /dev/null +++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java @@ -0,0 +1,76 @@ +/* + * 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.spring.boot; + +import java.util.Map; +import java.util.Properties; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +class EarlyResolutionPropertySourcesTest { + + @Test + void shouldExtractStringFromOriginTrackedValue() { + OriginTrackedValue tracked = OriginTrackedValue.of("{{vault:secret}}"); + assertThat(EarlyResolutionPropertySources.asString(tracked)).isEqualTo("{{vault:secret}}"); + } + + @Test + void shouldPreserveHighestPrecedenceValueForDuplicateKeys() { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("application-prod.properties", Map.of( + "my.secret", "{{vault:prod-secret}}"))); + sources.addLast(new MapPropertySource("application.properties", Map.of( + "my.secret", "{{vault:default-secret}}"))); + + Properties resolved = collectResolved(sources, value -> value.startsWith("{{vault:")); + + assertThat(resolved.getProperty("my.secret")).isEqualTo("resolved-prod-secret"); + } + + @Test + void shouldIgnoreLowerPrecedenceSourceWhenHigherPrecedenceAlreadyResolved() { + Properties props = new Properties(); + EarlyResolutionPropertySources.putIfAbsent(props, "key", "from-prod"); + EarlyResolutionPropertySources.putIfAbsent(props, "key", "from-default"); + + assertThat(props.getProperty("key")).isEqualTo("from-prod"); + } + + private static Properties collectResolved(MutablePropertySources sources, Predicate matchesPlaceholder) { + Properties props = new Properties(); + for (PropertySource propertySource : sources) { + if (propertySource instanceof MapPropertySource mapPropertySource) { + mapPropertySource.getSource().forEach((key, value) -> { + String stringValue = EarlyResolutionPropertySources.asString(value); + if (stringValue != null && matchesPlaceholder.test(stringValue)) { + String resolved = stringValue.contains("prod") ? "resolved-prod-secret" : "resolved-default-secret"; + EarlyResolutionPropertySources.putIfAbsent(props, key, resolved); + } + }); + } + } + return props; + } +} From 3bffd861b25240f36b27f0299d5a32e003e9bfde Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Fri, 28 Aug 2026 13:38:51 +0200 Subject: [PATCH 2/3] CAMEL-24532: fix import order in cyberark and jasypt parsers Co-Authored-By: Claude Sonnet 5 --- .../springboot/SpringBootCyberArkVaultPropertiesParser.java | 2 +- .../jasypt/springboot/SpringBootJasyptPropertiesParser.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java index 84ae1b0fdfb5..60908181ab9e 100644 --- a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java +++ b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java @@ -18,9 +18,9 @@ import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.cyberark.vault.CyberArkVaultPropertiesFunction; -import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.component.cyberark.vault.client.ConjurClient; import org.apache.camel.component.cyberark.vault.client.ConjurClientFactory; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java b/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java index 6367aefb5b21..5f90c1224d22 100644 --- a/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java +++ b/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java @@ -17,8 +17,8 @@ package org.apache.camel.component.jasypt.springboot; import org.apache.camel.component.jasypt.JasyptPropertiesParser; -import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.component.properties.PropertiesParser; +import org.apache.camel.spring.boot.EarlyResolutionPropertySources; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.StringHelper; import org.jasypt.encryption.StringEncryptor; From c108141e0dd645de301223d8ffe3314424f43327 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Aug 2026 22:34:06 +0000 Subject: [PATCH 3/3] CAMEL-24532: Skip early resolution when higher-precedence source defines key Add hasHigherPrecedenceProperty guard to EarlyResolutionPropertySources and apply it in all eight early-resolution parsers before contacting vault backends. This prevents lower-precedence placeholders from being resolved when a higher-precedence plain value already exists. Includes unit regression tests and a Hashicorp parser precedence test. Co-authored-by: Cursor --- ...BootAwsSecretsManagerPropertiesParser.java | 4 ++ ...ringBootAzureKeyVaultPropertiesParser.java | 4 ++ ...ringBootCyberArkVaultPropertiesParser.java | 4 ++ ...otGoogleSecretManagerPropertiesParser.java | 4 ++ ...ingBootHashicorpVaultPropertiesParser.java | 4 ++ ...rpVaultPropertiesParserPrecedenceTest.java | 69 +++++++++++++++++++ ...BMSecretsManagerVaultPropertiesParser.java | 4 ++ .../SpringBootJasyptPropertiesParser.java | 4 ++ ...SpringBootCloudConfigPropertiesParser.java | 4 ++ .../boot/EarlyResolutionPropertySources.java | 36 ++++++++++ .../EarlyResolutionPropertySourcesTest.java | 63 ++++++++++++++++- 11 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParserPrecedenceTest.java diff --git a/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java b/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java index c9fa0adc933b..b32f0c13ee43 100644 --- a/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java +++ b/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java @@ -85,6 +85,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { if (stringValue != null && stringValue.startsWith("{{aws:") && stringValue.endsWith("}}")) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { String element = secretsManagerPropertiesFunction.apply(stringValue diff --git a/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java b/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java index 24c13997c304..7655f90836f0 100644 --- a/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java +++ b/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java @@ -94,6 +94,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { if (stringValue != null && stringValue.startsWith("{{azure:") && stringValue.endsWith("}}")) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { String element = keyVaultPropertiesFunction.apply(stringValue diff --git a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java index 60908181ab9e..cb1bcff17ef1 100644 --- a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java +++ b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java @@ -83,6 +83,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { if (stringValue != null && stringValue.startsWith("{{cyberark:") && stringValue.endsWith("}}")) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { String element = cyberArkVaultPropertiesFunction.apply(stringValue diff --git a/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java b/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java index 9d2e778097be..25016a92b8ea 100644 --- a/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java +++ b/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java @@ -70,6 +70,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { if (stringValue != null && stringValue.startsWith("{{gcp:") && stringValue.endsWith("}}")) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { String element = secretsManagerPropertiesFunction.apply(stringValue diff --git a/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java b/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java index ccce593c58b6..5676c4f45b67 100644 --- a/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java +++ b/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java @@ -75,6 +75,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { if (stringValue != null && stringValue.startsWith("{{hashicorp:") && stringValue.endsWith("}}")) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { EarlyResolutionPropertySources.putIfAbsent(props, key, hashicorpVaultPropertiesFunction.apply(stringValue diff --git a/components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParserPrecedenceTest.java b/components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParserPrecedenceTest.java new file mode 100644 index 000000000000..a7d71c722ca5 --- /dev/null +++ b/components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParserPrecedenceTest.java @@ -0,0 +1,69 @@ +/* + * 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 java.util.Map; + +import org.apache.camel.test.infra.hashicorp.vault.services.HashicorpServiceFactory; +import org.apache.camel.test.infra.hashicorp.vault.services.HashicorpVaultService; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.bootstrap.DefaultBootstrapContext; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +class SpringBootHashicorpVaultPropertiesParserPrecedenceTest { + + @RegisterExtension + static HashicorpVaultService service = HashicorpServiceFactory.createService(); + + @BeforeAll + static void setupVaultConnection() { + 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"); + System.setProperty("camel.vault.hashicorp.token", service.token()); + } + + @Test + void shouldSkipMissingLowerPrecedencePlaceholderWhenHigherPrecedenceDefinesPlainValue() { + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("config", Map.of( + "camel.component.hashicorp-vault.early-resolve-properties", "true", + "camel.vault.hashicorp.host", service.host(), + "camel.vault.hashicorp.port", String.valueOf(service.port()), + "camel.vault.hashicorp.scheme", "http", + "camel.vault.hashicorp.token", service.token()))); + environment.getPropertySources().addFirst(new MapPropertySource("precedence-high", Map.of( + "precedence.test.key", "plain-value-from-high"))); + environment.getPropertySources().addLast(new MapPropertySource("precedence-low", Map.of( + "precedence.test.key", "{{hashicorp:does-not-exist#field}}"))); + + SpringBootHashicorpVaultPropertiesParser parser = new SpringBootHashicorpVaultPropertiesParser(); + ApplicationEnvironmentPreparedEvent event = new ApplicationEnvironmentPreparedEvent( + new DefaultBootstrapContext(), new SpringApplication(Object.class), new String[] {}, environment); + + assertThatCode(() -> parser.onApplicationEvent(event)).doesNotThrowAnyException(); + assertThat(environment.getProperty("precedence.test.key")).isEqualTo("plain-value-from-high"); + } +} diff --git a/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java b/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java index ca0da174a492..688ac2151583 100644 --- a/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java +++ b/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java @@ -68,6 +68,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { if (stringValue != null && stringValue.startsWith("{{ibm:") && stringValue.endsWith("}}")) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { String element = secretsManagerPropertiesFunction.apply(stringValue diff --git a/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java b/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java index 5f90c1224d22..daac0659a908 100644 --- a/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java +++ b/components-starter/camel-jasypt-starter/src/main/java/org/apache/camel/component/jasypt/springboot/SpringBootJasyptPropertiesParser.java @@ -74,6 +74,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { if (stringValue != null && stringValue.startsWith(JasyptPropertiesParser.JASYPT_PREFIX_TOKEN) && stringValue.endsWith(JasyptPropertiesParser.JASYPT_SUFFIX_TOKEN)) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { diff --git a/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java b/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java index 9f34bec54892..734102d5016f 100644 --- a/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java +++ b/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java @@ -52,6 +52,10 @@ public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && stringValue.startsWith("{{spring-config:") && stringValue.endsWith("}}")) { + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + event.getEnvironment().getPropertySources(), mutablePropertySources, key, LOG)) { + return; + } LOG.debug("decrypting and overriding property {}", key); try { String element = springCloudConfigPropertiesFunction diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java index bf7f1101cc31..6b2fabecad41 100644 --- a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java +++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/EarlyResolutionPropertySources.java @@ -18,7 +18,10 @@ import java.util.Properties; +import org.slf4j.Logger; import org.springframework.boot.origin.OriginTrackedValue; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; /** * Helpers for early property resolution listeners that inject a flat {@link Properties} override via @@ -54,4 +57,37 @@ public static String asString(Object value) { public static void putIfAbsent(Properties props, Object key, String resolvedValue) { props.putIfAbsent(key.toString(), resolvedValue); } + + /** + * Returns whether a property source with higher precedence defines the same key. + */ + public static boolean hasHigherPrecedenceProperty( + Iterable> propertySources, PropertySource currentPropertySource, Object key) { + for (PropertySource propertySource : propertySources) { + if (propertySource == currentPropertySource) { + return false; + } + if (propertySource instanceof MapPropertySource && propertySource.containsProperty(key.toString())) { + return true; + } + } + return false; + } + + /** + * Logs and returns {@code true} when early resolution should be skipped because a higher-precedence source + * already defines the key. + */ + public static boolean shouldSkipBecauseHigherPrecedenceDefines( + Iterable> propertySources, PropertySource currentPropertySource, Object key, + Logger log) { + if (hasHigherPrecedenceProperty(propertySources, currentPropertySource, key)) { + log.debug( + "Skipping early resolution for property {} from property source {} because a " + + "higher-precedence property source already defines it", + key, currentPropertySource.getName()); + return true; + } + return false; + } } diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java index 929eda7abdff..c99748a46890 100644 --- a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java +++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/EarlyResolutionPropertySourcesTest.java @@ -18,9 +18,11 @@ import java.util.Map; import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; @@ -58,15 +60,72 @@ void shouldIgnoreLowerPrecedenceSourceWhenHigherPrecedenceAlreadyResolved() { assertThat(props.getProperty("key")).isEqualTo("from-prod"); } + @Test + void shouldDetectHigherPrecedencePropertySource() { + MutablePropertySources sources = new MutablePropertySources(); + MapPropertySource high = new MapPropertySource("high", Map.of("shared.key", "plain")); + MapPropertySource low = new MapPropertySource("low", Map.of("shared.key", "{{hashicorp:missing#field}}")); + sources.addFirst(high); + sources.addLast(low); + + assertThat(EarlyResolutionPropertySources.hasHigherPrecedenceProperty(sources, high, "shared.key")).isFalse(); + assertThat(EarlyResolutionPropertySources.hasHigherPrecedenceProperty(sources, low, "shared.key")).isTrue(); + } + + @Test + void shouldSkipLowerPrecedencePlaceholderWhenHigherPrecedenceDefinesPlainValue() { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new MapPropertySource("high", Map.of("shared.key", "plain-value"))); + sources.addLast(new MapPropertySource("low", Map.of("shared.key", "{{hashicorp:missing#field}}"))); + + AtomicInteger resolutionAttempts = new AtomicInteger(); + Properties resolved = collectResolvedWithPrecedenceGuard(sources, value -> { + resolutionAttempts.incrementAndGet(); + return "must-not-be-used"; + }, value -> value.startsWith("{{hashicorp:")); + + assertThat(resolutionAttempts).hasValue(0); + assertThat(resolved).doesNotContainKey("shared.key"); + } + + @Test + void shouldLogSkipWithoutValueWhenHigherPrecedenceDefinesKey() { + MutablePropertySources sources = new MutablePropertySources(); + MapPropertySource high = new MapPropertySource("high", Map.of("shared.key", "plain")); + MapPropertySource low = new MapPropertySource("low", Map.of("shared.key", "{{vault:secret}}")); + sources.addFirst(high); + sources.addLast(low); + + assertThat(EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + sources, low, "shared.key", LoggerFactory.getLogger(EarlyResolutionPropertySourcesTest.class))) + .isTrue(); + } + private static Properties collectResolved(MutablePropertySources sources, Predicate matchesPlaceholder) { + return collectResolvedWithPrecedenceGuard(sources, value -> { + if (value.contains("prod")) { + return "resolved-prod-secret"; + } + return "resolved-default-secret"; + }, matchesPlaceholder); + } + + private static Properties collectResolvedWithPrecedenceGuard( + MutablePropertySources sources, + java.util.function.Function resolver, + Predicate matchesPlaceholder) { Properties props = new Properties(); + var log = LoggerFactory.getLogger(EarlyResolutionPropertySourcesTest.class); for (PropertySource propertySource : sources) { if (propertySource instanceof MapPropertySource mapPropertySource) { mapPropertySource.getSource().forEach((key, value) -> { String stringValue = EarlyResolutionPropertySources.asString(value); if (stringValue != null && matchesPlaceholder.test(stringValue)) { - String resolved = stringValue.contains("prod") ? "resolved-prod-secret" : "resolved-default-secret"; - EarlyResolutionPropertySources.putIfAbsent(props, key, resolved); + if (EarlyResolutionPropertySources.shouldSkipBecauseHigherPrecedenceDefines( + sources, propertySource, key, log)) { + return; + } + EarlyResolutionPropertySources.putIfAbsent(props, key, resolver.apply(stringValue)); } }); }