This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
The following commit(s) were added to refs/heads/main by this push: new dbfc100e9e8 CAMEL-21265 - Create ApplicationEnvironmentPreparedEvent for Vault components - Azure Key Vault (#1259) dbfc100e9e8 is described below commit dbfc100e9e8ad24ba02453d6940cf555f51f6ee1 Author: Andrea Cosentino <anco...@gmail.com> AuthorDate: Thu Oct 17 12:19:45 2024 +0200 CAMEL-21265 - Create ApplicationEnvironmentPreparedEvent for Vault components - Azure Key Vault (#1259) Signed-off-by: Andrea Cosentino <anco...@gmail.com> --- .../camel-azure-key-vault-starter/pom.xml | 12 ++ .../SpringBootAzureKeyVaultPropertiesParser.java | 116 +++++++++++++++++++ .../src/main/resources/META-INF/spring.factories | 2 + .../springboot/EarlyResolvedPropertiesTest.java | 123 +++++++++++++++++++++ .../src/test/resources/application.properties | 2 + 5 files changed, 255 insertions(+) diff --git a/components-starter/camel-azure-key-vault-starter/pom.xml b/components-starter/camel-azure-key-vault-starter/pom.xml index 897675699a2..695cf59afb2 100644 --- a/components-starter/camel-azure-key-vault-starter/pom.xml +++ b/components-starter/camel-azure-key-vault-starter/pom.xml @@ -38,6 +38,18 @@ <artifactId>camel-azure-key-vault</artifactId> <version>${camel-version}</version> </dependency> + <!-- for testing --> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <version>${spring-boot-version}</version> + <scope>test</scope> + </dependency> <!--START OF GENERATED CODE--> <dependency> <groupId>org.apache.camel.springboot</groupId> 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 new file mode 100644 index 00000000000..221d2f8a83b --- /dev/null +++ b/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java @@ -0,0 +1,116 @@ +/* + * 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.azure.key.vault.springboot; + +import com.azure.core.credential.TokenCredential; +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.security.keyvault.secrets.SecretClient; +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.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; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; + +import java.util.Properties; + +public class SpringBootAzureKeyVaultPropertiesParser implements ApplicationListener<ApplicationEnvironmentPreparedEvent> { + private static final Logger LOG = LoggerFactory.getLogger(SpringBootAzureKeyVaultPropertiesParser.class); + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { + SecretClient client; + ConfigurableEnvironment environment = event.getEnvironment(); + if (Boolean.parseBoolean(environment.getProperty("camel.component.azure-key-vault.early-resolve-properties"))) { + String vaultName = environment.getProperty("camel.vault.azure.vaultName"); + String clientId = environment.getProperty("camel.vault.azure.clientId"); + String clientSecret = environment.getProperty("camel.vault.azure.clientSecret"); + String tenantId = environment.getProperty("camel.vault.azure.tenantId"); + boolean azureIdentityEnabled = Boolean.parseBoolean(System.getenv("camel.vault.azure.azureIdentityEnabled")); + if (ObjectHelper.isNotEmpty(vaultName) && ObjectHelper.isNotEmpty(clientId) && ObjectHelper.isNotEmpty(clientSecret) + && ObjectHelper.isNotEmpty(tenantId) && !azureIdentityEnabled) { + String keyVaultUri = "https://" + vaultName + ".vault.azure.net"; + + // Credential + ClientSecretCredential credential = new ClientSecretCredentialBuilder() + .tenantId(tenantId) + .clientId(clientId) + .clientSecret(clientSecret) + .build(); + + // Build Client + client = new SecretClientBuilder() + .vaultUrl(keyVaultUri) + .credential(credential) + .buildClient(); + } else if (ObjectHelper.isNotEmpty(vaultName) && azureIdentityEnabled) { + String keyVaultUri = "https://" + vaultName + ".vault.azure.net"; + + // Credential + TokenCredential credential = new DefaultAzureCredentialBuilder().build(); + + // Build Client + client = new SecretClientBuilder() + .vaultUrl(keyVaultUri) + .credential(credential) + .buildClient(); + } else { + throw new RuntimeCamelException( + "Using the Azure Key Vault Properties Function requires setting Azure credentials as application properties or environment variables or enable the Azure Identity Authentication mechanism"); + } + KeyVaultPropertiesFunction keyVaultPropertiesFunction = new KeyVaultPropertiesFunction(client); + final Properties props = new Properties(); + 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; + } + if (stringValue != null && + stringValue.startsWith("{{azure:") && + stringValue.endsWith("}}")) { + LOG.debug("decrypting and overriding property {}", key); + try { + String element = keyVaultPropertiesFunction.apply(stringValue + .replace("{{azure:", "") + .replace("}}", "")); + props.put(key, element); + } catch (Exception e) { + // Log and do nothing + LOG.debug("failed to parse property {}. This exception is ignored.", key, e); + } + } + }); + } + } + environment.getPropertySources().addFirst(new PropertiesPropertySource("overridden-camel-azure-key-vault-properties", props)); + } + } +} diff --git a/components-starter/camel-azure-key-vault-starter/src/main/resources/META-INF/spring.factories b/components-starter/camel-azure-key-vault-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..13858c09621 --- /dev/null +++ b/components-starter/camel-azure-key-vault-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationListener=\ + org.apache.camel.component.azure.key.vault.springboot.SpringBootAzureKeyVaultPropertiesParser \ No newline at end of file diff --git a/components-starter/camel-azure-key-vault-starter/src/test/java/org/apache/camel/component/azure/key/vault/springboot/EarlyResolvedPropertiesTest.java b/components-starter/camel-azure-key-vault-starter/src/test/java/org/apache/camel/component/azure/key/vault/springboot/EarlyResolvedPropertiesTest.java new file mode 100644 index 00000000000..80641f4aecf --- /dev/null +++ b/components-starter/camel-azure-key-vault-starter/src/test/java/org/apache/camel/component/azure/key/vault/springboot/EarlyResolvedPropertiesTest.java @@ -0,0 +1,123 @@ +/* + * 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.azure.key.vault.springboot; + +import com.azure.core.util.polling.SyncPoller; +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.SecretClientBuilder; +import com.azure.security.keyvault.secrets.models.DeletedSecret; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import org.apache.camel.spring.boot.CamelAutoConfiguration; +import org.apache.camel.test.spring.junit5.CamelSpringBootTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperties; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +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 java.io.IOException; + +@CamelSpringBootTest +@DirtiesContext +@SpringBootApplication +@SpringBootTest( + classes = { EarlyResolvedPropertiesTest.TestConfiguration.class }, + properties = { + "camel.component.azure-key-vault.early-resolve-properties=true", + "early.resolved.property.simple={{azure:dbTestPassword}}" + }) + +// Must be manually tested. Provide your own projectId using -Dcamel.vault.test.azure.tenantId -Dcamel.vault.test.azure.clientId -Dcamel.vault.test.azure.clientSecret -Dcamel.vault.test.azure.vaultName +@EnabledIfSystemProperties({ + @EnabledIfSystemProperty(named = "camel.vault.test.azure.tenantId", matches = ".*", + disabledReason = "Azure Tenant Id not provided"), + @EnabledIfSystemProperty(named = "camel.vault.test.azure.clientId", matches = ".*", + disabledReason = "Azure Key Vault Client Id not provided"), + @EnabledIfSystemProperty(named = "camel.vault.test.azure.clientSecret", matches = ".*", + disabledReason = "Azure Key Vault Client Secret not provided"), + @EnabledIfSystemProperty(named = "camel.vault.test.azure.vaultName", matches = ".*", + disabledReason = "Azure Key Vault Name not provided"), +}) +public class EarlyResolvedPropertiesTest { + + static SecretClient client; + static String secretId; + + @BeforeAll + public static void setup() throws IOException { + String tenantId = System.getProperty("camel.vault.test.azure.tenantId"); + String clientId = System.getProperty("camel.vault.test.azure.clientId"); + String clientSecret = System.getProperty("camel.vault.test.azure.clientSecret"); + String vaultName = System.getProperty("camel.vault.test.azure.vaultName"); + System.setProperty("camel.vault.azure.tenantId", tenantId); + System.setProperty("camel.vault.azure.clientId", clientId); + System.setProperty("camel.vault.azure.clientSecret", clientSecret); + System.setProperty("camel.vault.azure.vaultName", vaultName); + + String keyVaultUri = "https://" + vaultName + ".vault.azure.net"; + + // Credential + ClientSecretCredential credential = new ClientSecretCredentialBuilder() + .tenantId(tenantId) + .clientId(clientId) + .clientSecret(clientSecret) + .build(); + + // Build Client + client = new SecretClientBuilder() + .vaultUrl(keyVaultUri) + .credential(credential) + .buildClient(); + + KeyVaultSecret p = client. + setSecret(new KeyVaultSecret("dbTestPassword", "string")); + } + + @AfterAll + public static void teardown() throws IOException { + SyncPoller<DeletedSecret, Void> p = client + .beginDeleteSecret("dbTestPassword"); + p.waitForCompletion(); + client.purgeDeletedSecret("dbTestPassword"); + } + + @Value("${early.resolved.property}") + private String earlyResolvedProperty; + + @Value("${early.resolved.property.simple}") + private String earlyResolvedPropertySimple; + + @Test + public void testEarlyResolvedProperties() { + Assertions.assertThat(earlyResolvedProperty).isEqualTo("string"); + Assertions.assertThat(earlyResolvedPropertySimple).isEqualTo("string"); + } + + @Configuration + @AutoConfigureBefore(CamelAutoConfiguration.class) + public static class TestConfiguration { + } +} diff --git a/components-starter/camel-azure-key-vault-starter/src/test/resources/application.properties b/components-starter/camel-azure-key-vault-starter/src/test/resources/application.properties new file mode 100644 index 00000000000..21f525109e6 --- /dev/null +++ b/components-starter/camel-azure-key-vault-starter/src/test/resources/application.properties @@ -0,0 +1,2 @@ +# Needed by EarlyResolvedPropertiesTest +early.resolved.property = {{azure:dbTestPassword}} \ No newline at end of file