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.git
commit c7ca25455cdecc8cda04b79aa283499e53077098 Author: Andrea Cosentino <anco...@gmail.com> AuthorDate: Wed Jul 13 19:13:40 2022 +0200 CAMEL-17688 - Support ability to load properties from Vault/Secrets cloud services - Hashicorp Vault --- .../org/apache/camel/properties-function/hashicorp | 2 + .../vault/HashicorpVaultPropertiesFunction.java | 221 +++++++++++++++++++++ .../HashicorpVaultPropertiesSourceTestIT.java | 72 +++++++ 3 files changed, 295 insertions(+) diff --git a/components/camel-hashicorp-vault/src/generated/resources/META-INF/services/org/apache/camel/properties-function/hashicorp b/components/camel-hashicorp-vault/src/generated/resources/META-INF/services/org/apache/camel/properties-function/hashicorp new file mode 100644 index 00000000000..1bc4dea7180 --- /dev/null +++ b/components/camel-hashicorp-vault/src/generated/resources/META-INF/services/org/apache/camel/properties-function/hashicorp @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.hashicorp.vault.HashicorpVaultPropertiesFunction diff --git a/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java new file mode 100644 index 00000000000..673a94e956a --- /dev/null +++ b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java @@ -0,0 +1,221 @@ +/* + * 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; + +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.spi.PropertiesFunction; +import org.apache.camel.support.service.ServiceSupport; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.StringHelper; +import org.apache.camel.vault.HashicorpVaultConfiguration; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.support.VaultResponse; + +/** + * A {@link PropertiesFunction} that lookup the property value from AWS Secrets Manager service. + * <p/> + * The credentials to access Secrets Manager is defined using three environment variables representing the static + * credentials: + * <ul> + * <li><tt>CAMEL_VAULT_AWS_ACCESS_KEY</tt></li> + * <li><tt>CAMEL_VAULT_AWS_SECRET_KEY</tt></li> + * <li><tt>CAMEL_VAULT_AWS_REGION</tt></li> + * <li><tt>CAMEL_VAULT_AWS_USE_DEFAULT_CREDENTIALS_PROVIDER</tt></li> + * </ul> + * <p/> + * + * Otherwise it is possible to specify the credentials as properties: + * + * <ul> + * <li><tt>camel.vault.aws.accessKey</tt></li> + * <li><tt>camel.vault.aws.secretKey</tt></li> + * <li><tt>camel.vault.aws.region</tt></li> + * <li><tt>camel.vault.aws.useDefaultCredentialsProvider</tt></li> + * </ul> + * <p/> + * + * This implementation is to return the secret value associated with a key. The properties related to this kind of + * Properties Function are all prefixed with <tt>aws:</tt>. For example asking for <tt>aws:token</tt>, will return the + * secret value associated to the secret named token on AWS Secrets Manager. + * + * Another way of retrieving a secret value is using the following notation <tt>aws:database/username</tt>: in this case + * the field username of the secret database will be returned. As a fallback, the user could provide a default value, + * which will be returned in case the secret doesn't exist, the secret has been marked for deletion or, for example, if + * a particular field of the secret doesn't exist. For using this feature, the user could use the following notation + * <tt>aws:database/username:admin</tt>. The admin value will be returned as default value, if the conditions above were + * all met. + */ + +@org.apache.camel.spi.annotations.PropertiesFunction("hashicorp") +public class HashicorpVaultPropertiesFunction extends ServiceSupport implements PropertiesFunction, CamelContextAware { + + private static final String CAMEL_HASHICORP_VAULT_TOKEN_ENV = "CAMEL_HASHICORP_VAULT_TOKEN_ENV"; + private static final String CAMEL_HASHICORP_VAULT_ENGINE_ENV = "CAMEL_HASHICORP_VAULT_ENGINE_ENV"; + private static final String CAMEL_HASHICORP_VAULT_HOST_ENV = "CAMEL_HASHICORP_VAULT_HOST_ENV"; + private static final String CAMEL_HASHICORP_VAULT_PORT_ENV + = "CAMEL_HASHICORP_VAULT_PORT_ENV"; + private static final String CAMEL_HASHICORP_VAULT_SCHEME_ENV + = "CAMEL_HASHICORP_VAULT_SCHEME_ENV"; + private CamelContext camelContext; + private VaultTemplate client; + + protected String engine; + + @Override + protected void doStart() throws Exception { + super.doStart(); + String token = System.getenv(CAMEL_HASHICORP_VAULT_TOKEN_ENV); + engine = System.getenv(CAMEL_HASHICORP_VAULT_ENGINE_ENV); + String host = System.getenv(CAMEL_HASHICORP_VAULT_HOST_ENV); + String port = System.getenv(CAMEL_HASHICORP_VAULT_PORT_ENV); + String scheme = System.getenv(CAMEL_HASHICORP_VAULT_SCHEME_ENV); + if (ObjectHelper.isEmpty(token) && ObjectHelper.isEmpty(engine) && ObjectHelper.isEmpty(host) && ObjectHelper.isEmpty(port) && ObjectHelper.isEmpty(scheme)) { + HashicorpVaultConfiguration hashicorpVaultConfiguration = getCamelContext().getVaultConfiguration().hashicorp(); + if (ObjectHelper.isNotEmpty(hashicorpVaultConfiguration)) { + token = hashicorpVaultConfiguration.getToken(); + engine = hashicorpVaultConfiguration.getEngine(); + host = hashicorpVaultConfiguration.getHost(); + port = hashicorpVaultConfiguration.getPort(); + scheme = hashicorpVaultConfiguration.getScheme(); + } + } + if (ObjectHelper.isNotEmpty(token) && ObjectHelper.isNotEmpty(engine) && ObjectHelper.isNotEmpty(host) && ObjectHelper.isNotEmpty(port) && ObjectHelper.isNotEmpty(scheme)) { + VaultEndpoint vaultEndpoint = new VaultEndpoint(); + vaultEndpoint.setHost(host); + vaultEndpoint.setPort(Integer.parseInt(port)); + vaultEndpoint.setScheme(scheme); + + client = new VaultTemplate( + vaultEndpoint, + new TokenAuthentication(token)); + } else { + throw new RuntimeCamelException( + "Using the Hashicorp Properties Function requires setting Engine, Token, Host, port and scheme properties"); + } + } + + @Override + protected void doStop() throws Exception { + super.doStop(); + } + + @Override + public String getName() { + return "hashicorp"; + } + + @Override + public String apply(String remainder) { + String key = remainder; + String subkey = null; + String returnValue = null; + String defaultValue = null; + String version = null; + if (remainder.contains("/")) { + key = StringHelper.before(remainder, "/"); + subkey = StringHelper.after(remainder, "/"); + defaultValue = StringHelper.after(subkey, ":"); + if (ObjectHelper.isNotEmpty(defaultValue)) { + if (defaultValue.contains("@")) { + version = StringHelper.after(defaultValue, "@"); + defaultValue = StringHelper.before(defaultValue, "@"); + } + } + if (subkey.contains(":")) { + subkey = StringHelper.before(subkey, ":"); + } + if (subkey.contains("@")) { + version = StringHelper.after(subkey, "@"); + subkey = StringHelper.before(subkey, "@"); + } + } else if (remainder.contains(":")) { + key = StringHelper.before(remainder, ":"); + defaultValue = StringHelper.after(remainder, ":"); + if (remainder.contains("@")) { + version = StringHelper.after(remainder, "@"); + defaultValue = StringHelper.before(defaultValue, "@"); + } + } else { + if (remainder.contains("@")) { + key = StringHelper.before(remainder, "@"); + version = StringHelper.after(remainder, "@"); + } + } + + if (key != null) { + try { + returnValue = getSecretFromSource(key, subkey, defaultValue, version); + } catch (JsonProcessingException e) { + throw new RuntimeCamelException("Something went wrong while recovering " + key + " from vault"); + } + } + + return returnValue; + } + + private String getSecretFromSource( + String key, String subkey, String defaultValue, String version) + throws JsonProcessingException { + String returnValue = null; + try { + String completePath = engine + "/" + "data" + "/" + key; + if (ObjectHelper.isNotEmpty(version)) { + completePath = completePath + "?version=" + version; + } + VaultResponse rawSecret = client.read(completePath); + if (ObjectHelper.isNotEmpty(rawSecret)) { + returnValue = rawSecret.getData().get("data").toString(); + } + if (ObjectHelper.isNotEmpty(subkey)) { + Object field = ((Map) rawSecret.getData().get("data")).get(subkey); + if (ObjectHelper.isNotEmpty(field)) { + returnValue = field.toString(); + } else { + returnValue = null; + } + } + if (ObjectHelper.isEmpty(returnValue)) { + returnValue = defaultValue; + } + } catch (Exception ex) { + if (ObjectHelper.isNotEmpty(defaultValue)) { + returnValue = defaultValue; + } else { + throw ex; + } + } + return returnValue; + } + + @Override + public void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + } + + @Override + public CamelContext getCamelContext() { + return camelContext; + } +} + diff --git a/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/integration/HashicorpVaultPropertiesSourceTestIT.java b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/integration/HashicorpVaultPropertiesSourceTestIT.java new file mode 100644 index 00000000000..beacf7b95cc --- /dev/null +++ b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/integration/HashicorpVaultPropertiesSourceTestIT.java @@ -0,0 +1,72 @@ +/* + * 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.integration; + +import org.apache.camel.FailedToCreateRouteException; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HashicorpVaultPropertiesSourceTestIT extends CamelTestSupport { + + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_TOKEN_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_ENGINE_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_HOST_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_PORT_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_SCHEME_ENV", matches = ".*") + @Test + public void testFunctio() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:start").setBody(simple("{{hashicorp:hello}}")).to("mock:bar"); + } + }); + context.start(); + + getMockEndpoint("mock:bar").expectedBodiesReceived("{id=21}"); + + template.sendBody("direct:start", "Hello World"); + + assertMockEndpointsSatisfied(); + } + + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_TOKEN_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_ENGINE_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_HOST_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_PORT_ENV", matches = ".*") + @EnabledIfEnvironmentVariable(named = "CAMEL_HASHICORP_VAULT_SCHEME_ENV", matches = ".*") + @Test + public void testFunctionWithField() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:start").setBody(simple("{{hashicorp:hello/id}}")).to("mock:bar"); + } + }); + context.start(); + + getMockEndpoint("mock:bar").expectedBodiesReceived("21"); + + template.sendBody("direct:start", "Hello World"); + + assertMockEndpointsSatisfied(); + } +}