NIFI-9401 Added HashiCorpVaultParameterProvider

- Refactored nifi-vault-utils to nifi-hashicorp-vault-api and nifi-hashcorp-vault modules
- Added HashiCorpVaultClientService and Standard implementation

This closes #6304

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Joe Gresock 2022-09-09 14:19:46 -04:00 committed by exceptionfactory
parent 5303aadda3
commit 3987d39cdc
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
36 changed files with 1423 additions and 13 deletions

View File

@ -838,6 +838,12 @@ language governing permissions and limitations under the License. -->
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-client-service-api-nar</artifactId>
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-stateless-processor-nar</artifactId>

View File

@ -0,0 +1,24 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-hashicorp-vault-api</artifactId>
</project>

View File

@ -16,6 +16,7 @@
*/
package org.apache.nifi.vault.hashicorp;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -24,6 +25,10 @@ import java.util.Optional;
* @see <a href="https://www.vaultproject.io/">https://www.vaultproject.io/</a>
*/
public interface HashiCorpVaultCommunicationService {
/**
* @return The HashiCorp Vault server version
*/
String getServerVersion();
/**
* Encrypts the given plaintext using Vault's Transit Secrets Engine.
@ -83,4 +88,11 @@ public interface HashiCorpVaultCommunicationService {
* @return A map from key to value from the secret key/values, or an empty map if not found
*/
Map<String, String> readKeyValueSecretMap(String keyValuePath, String secretKey);
/**
* Lists the secrets at the given Key/Value Secrets Engine path.
* @param keyValuePath The Vault path to list
* @return The list of secret names
*/
List<String> listKeyValueSecrets(String keyValuePath);
}

View File

@ -19,7 +19,7 @@
<artifactId>nifi-commons</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-vault-utils</artifactId>
<artifactId>nifi-hashicorp-vault</artifactId>
<properties>
<spring.vault.version>2.3.2</spring.vault.version>
</properties>
@ -45,6 +45,12 @@
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>

View File

@ -34,6 +34,7 @@ import org.springframework.vault.support.VaultResponseSupport;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@ -64,6 +65,11 @@ public class StandardHashiCorpVaultCommunicationService implements HashiCorpVaul
keyValueOperationsMap = new HashMap<>();
}
@Override
public String getServerVersion() {
return vaultTemplate.opsForSys().health().getVersion();
}
/**
* Creates a VaultCommunicationService that uses Spring Vault.
* @param vaultProperties Properties to configure the service
@ -136,6 +142,13 @@ public class StandardHashiCorpVaultCommunicationService implements HashiCorpVaul
return response == null ? Collections.emptyMap() : (Map<String, String>) response.getRequiredData();
}
@Override
public List<String> listKeyValueSecrets(final String keyValuePath) {
final VaultKeyValueOperations keyValueOperations = keyValueOperationsMap
.computeIfAbsent(keyValuePath, path -> vaultTemplate.opsForKeyValue(path, KeyValueBackend.KV_1));
return keyValueOperations.list("/");
}
private static class SecretData {
private final String value;

View File

@ -32,6 +32,7 @@ import org.springframework.vault.support.SslConfiguration;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
@ -70,13 +71,14 @@ public class HashiCorpVaultConfiguration extends EnvironmentVaultConfiguration {
private final KeyValueBackend keyValueBackend;
/**
* Creates a HashiCorpVaultConfiguration from property sources
* @param propertySources A series of Spring PropertySource objects
* Creates a HashiCorpVaultConfiguration from property sources, in increasing precedence.
* @param propertySources A series of Spring PropertySource objects (the last in the list take precedence over
* sources earlier in the list)
* @throws HashiCorpVaultConfigurationException If the authentication properties file could not be read
*/
public HashiCorpVaultConfiguration(final PropertySource<?>... propertySources) {
final ConfigurableEnvironment env = new StandardEnvironment();
for(final PropertySource<?> propertySource : propertySources) {
for (final PropertySource<?> propertySource : propertySources) {
env.getPropertySources().addFirst(propertySource);
}
@ -105,6 +107,7 @@ public class HashiCorpVaultConfiguration extends EnvironmentVaultConfiguration {
}
}
this.keyValueBackend = keyValueBackend;
validateProperties(env);
this.setApplicationContext(new HashiCorpVaultApplicationContext(env));
@ -114,6 +117,30 @@ public class HashiCorpVaultConfiguration extends EnvironmentVaultConfiguration {
clientOptions = getClientOptions();
}
private void validateProperties(final ConfigurableEnvironment environment) {
try {
final String vaultUri = Objects.requireNonNull(environment.getProperty(VaultConfigurationKey.URI.key),
"Missing required property " + VaultConfigurationKey.URI.key);
if (vaultUri.startsWith(HTTPS)) {
requireSslProperty("vault.ssl.key-store", environment);
requireSslProperty("vault.ssl.key-store-password", environment);
requireSslProperty("vault.ssl.key-store-type", environment);
requireSslProperty("vault.ssl.trust-store", environment);
requireSslProperty("vault.ssl.trust-store-password", environment);
requireSslProperty("vault.ssl.trust-store-type", environment);
}
} catch (final NullPointerException e) {
// Rethrow as IllegalArgumentException
throw new IllegalArgumentException(e.getMessage(), e);
}
}
private void requireSslProperty(final String propertyName, final ConfigurableEnvironment environment) {
Objects.requireNonNull(environment.getProperty(propertyName), propertyName + " is required with an https URI");
}
public KeyValueBackend getKeyValueBackend() {
return keyValueBackend;
}
@ -158,7 +185,7 @@ public class HashiCorpVaultConfiguration extends EnvironmentVaultConfiguration {
return new ClientOptions(connectionTimeoutDuration, readTimeoutDuration);
}
private static Duration getDuration(String formattedDuration) {
private static Duration getDuration(final String formattedDuration) {
final double duration = FormatUtils.getPreciseTimeDuration(formattedDuration, TimeUnit.MILLISECONDS);
return Duration.ofMillis(Double.valueOf(duration).longValue());
}

View File

@ -68,8 +68,8 @@ public class TestStandardHashiCorpVaultCommunicationService {
public void testBasicConfiguration() {
this.configureService();
// Once to check if the URI is https, and once by VaultTemplate
Mockito.verify(properties, Mockito.times(2)).getUri();
// Once to check if the URI is https, once by VaultTemplate, and once to validate
Mockito.verify(properties, Mockito.times(3)).getUri();
// Once to check if the property is set, and once to retrieve the value
Mockito.verify(properties, Mockito.times(2)).getAuthPropertiesFilename();
@ -85,8 +85,6 @@ public class TestStandardHashiCorpVaultCommunicationService {
Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getTrustStore();
Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getTrustStoreType();
Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getTrustStorePassword();
Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getEnabledProtocols();
Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getEnabledCipherSuites();
}
@Test
@ -113,6 +111,8 @@ public class TestStandardHashiCorpVaultCommunicationService {
when(properties.getUri()).thenReturn(URI_VALUE.replace("http", "https"));
this.configureService();
this.ensureTlsPropertiesAccessed(1);
this.ensureTlsPropertiesAccessed(2);
Mockito.verify(sslProperties, Mockito.times(1)).getEnabledProtocols();
Mockito.verify(sslProperties, Mockito.times(1)).getEnabledCipherSuites();
}
}

View File

@ -29,7 +29,12 @@
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-vault-utils</artifactId>
<artifactId>nifi-hashicorp-vault-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>

View File

@ -64,7 +64,8 @@
<module>nifi-socket-utils</module>
<module>nifi-utils</module>
<module>nifi-uuid5</module>
<module>nifi-vault-utils</module>
<module>nifi-hashicorp-vault</module>
<module>nifi-hashicorp-vault-api</module>
<module>nifi-web-client</module>
<module>nifi-web-client-api</module>
<module>nifi-web-utils</module>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-hashicorp-vault-client-service-api-nar</artifactId>
<packaging>nar</packaging>
<properties>
<maven.javadoc.skip>true</maven.javadoc.skip>
<source.skip>true</source.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-standard-services-api-nar</artifactId>
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-client-service-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-hashicorp-vault-client-service-api</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-ssl-context-service-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,115 @@
/*
* 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.nifi.vault.hashicorp;
import org.apache.nifi.components.AllowableValue;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.resource.ResourceCardinality;
import org.apache.nifi.components.resource.ResourceType;
import org.apache.nifi.controller.ControllerService;
import org.apache.nifi.controller.VerifiableControllerService;
import org.apache.nifi.expression.ExpressionLanguageScope;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.ssl.SSLContextService;
/**
* Provides a HashiCorpVaultCommunicationService.
*/
public interface HashiCorpVaultClientService extends ControllerService, VerifiableControllerService {
AllowableValue DIRECT_PROPERTIES = new AllowableValue("direct-properties", "Direct Properties",
"Use properties, including dynamic properties, configured directly in the Controller Service to configure the client");
AllowableValue PROPERTIES_FILES = new AllowableValue("properties-files", "Properties Files",
"Use one or more '.properties' files to configure the client");
PropertyDescriptor CONFIGURATION_STRATEGY = new PropertyDescriptor.Builder()
.displayName("Configuration Strategy")
.name("configuration-strategy")
.required(true)
.allowableValues(DIRECT_PROPERTIES, PROPERTIES_FILES)
.defaultValue(DIRECT_PROPERTIES.getValue())
.description("Specifies the source of the configuration properties.")
.build();
PropertyDescriptor VAULT_URI = new PropertyDescriptor.Builder()
.name("vault.uri")
.displayName("Vault URI")
.description("The URI of the HashiCorp Vault server (e.g., http://localhost:8200). Required if not specified in the " +
"Bootstrap HashiCorp Vault Configuration File.")
.required(true)
.expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
.addValidator(StandardValidators.URI_VALIDATOR)
.dependsOn(CONFIGURATION_STRATEGY, DIRECT_PROPERTIES)
.build();
PropertyDescriptor VAULT_AUTHENTICATION = new PropertyDescriptor.Builder()
.name("vault.authentication")
.displayName("Vault Authentication")
.description("Vault authentication method, as described in the Spring Vault Environment Configuration documentation " +
"(https://docs.spring.io/spring-vault/docs/2.3.x/reference/html/#vault.core.environment-vault-configuration).")
.required(true)
.allowableValues(VaultAuthenticationMethod.values())
.defaultValue(VaultAuthenticationMethod.TOKEN.name())
.dependsOn(CONFIGURATION_STRATEGY, DIRECT_PROPERTIES)
.build();
PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder()
.name("vault.ssl.context.service")
.displayName("SSL Context Service")
.description("The SSL Context Service used to provide client certificate information for TLS/SSL connections to the " +
"HashiCorp Vault server.")
.required(false)
.identifiesControllerService(SSLContextService.class)
.dependsOn(CONFIGURATION_STRATEGY, DIRECT_PROPERTIES)
.build();
PropertyDescriptor VAULT_PROPERTIES_FILES = new PropertyDescriptor.Builder()
.name("vault.properties.files")
.displayName("Vault Properties Files")
.description("A comma-separated list of files containing HashiCorp Vault configuration properties, as described in the Spring Vault " +
"Environment Configuration documentation (https://docs.spring.io/spring-vault/docs/2.3.x/reference/html/#vault.core.environment-vault-configuration). " +
"All of the Spring property keys and authentication-specific property keys are supported.")
.required(true)
.dependsOn(CONFIGURATION_STRATEGY, PROPERTIES_FILES)
.identifiesExternalResource(ResourceCardinality.MULTIPLE, ResourceType.FILE)
.build();
PropertyDescriptor CONNECTION_TIMEOUT = new PropertyDescriptor.Builder()
.name("vault.connection.timeout")
.displayName("Connection Timeout")
.description("The connection timeout for the HashiCorp Vault client")
.required(true)
.defaultValue("5 sec")
.addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
.build();
PropertyDescriptor READ_TIMEOUT = new PropertyDescriptor.Builder()
.name("vault.read.timeout")
.displayName("Read Timeout")
.description("The read timeout for the HashiCorp Vault client")
.required(true)
.defaultValue("15 sec")
.addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
.build();
/**
*
* @return A service for communicating with HashiCorp Vault.
*/
HashiCorpVaultCommunicationService getHashiCorpVaultCommunicationService();
}

View File

@ -0,0 +1,28 @@
/*
* 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.nifi.vault.hashicorp;
public enum VaultAuthenticationMethod {
TOKEN,
APPID,
APPROLE,
AWS_EC2,
AZURE,
CERT,
CUBBYHOLE,
KUBERNETES
}

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-hashicorp-vault-client-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-ssl-context-service-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-client-service-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-security-utils</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,209 @@
/*
* 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.nifi.vault.hashicorp;
import org.apache.nifi.annotation.behavior.DynamicProperties;
import org.apache.nifi.annotation.behavior.DynamicProperty;
import org.apache.nifi.annotation.behavior.SupportsSensitiveDynamicProperties;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnDisabled;
import org.apache.nifi.annotation.lifecycle.OnEnabled;
import org.apache.nifi.components.ConfigVerificationResult;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.resource.ResourceReference;
import org.apache.nifi.controller.AbstractControllerService;
import org.apache.nifi.controller.ConfigurationContext;
import org.apache.nifi.expression.ExpressionLanguageScope;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.reporting.InitializationException;
import org.apache.nifi.ssl.SSLContextService;
import org.apache.nifi.vault.hashicorp.config.HashiCorpVaultConfiguration;
import org.springframework.core.env.PropertySource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Tags({"hashicorp", "vault", "client"})
@CapabilityDescription("A controller service for interacting with HashiCorp Vault.")
@SupportsSensitiveDynamicProperties
@DynamicProperties(
@DynamicProperty(name = "A Spring Vault configuration property name",
value = "The property value",
description = "Allows any Spring Vault property keys to be specified, as described in " +
"(https://docs.spring.io/spring-vault/docs/2.3.x/reference/html/#vault.core.environment-vault-configuration). " +
"See Additional Details for more information.",
expressionLanguageScope = ExpressionLanguageScope.VARIABLE_REGISTRY
)
)
public class StandardHashiCorpVaultClientService extends AbstractControllerService implements HashiCorpVaultClientService {
private static List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
CONFIGURATION_STRATEGY,
VAULT_URI,
VAULT_AUTHENTICATION,
SSL_CONTEXT_SERVICE,
VAULT_PROPERTIES_FILES,
CONNECTION_TIMEOUT,
READ_TIMEOUT
));
private HashiCorpVaultCommunicationService communicationService;
@Override
protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
return new PropertyDescriptor.Builder()
.name(propertyDescriptorName)
.displayName(propertyDescriptorName)
.dynamic(true)
.expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
}
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
return PROPERTIES;
}
@Override
public List<ConfigVerificationResult> verify(final ConfigurationContext context, final ComponentLog verificationLogger,
final Map<String, String> variables) {
final List<ConfigVerificationResult> results = new ArrayList<>();
HashiCorpVaultCommunicationService service = null;
try {
service = createCommunicationService(context);
results.add(new ConfigVerificationResult.Builder()
.outcome(ConfigVerificationResult.Outcome.SUCCESSFUL)
.verificationStepName("Configure HashiCorp Vault Client")
.explanation("Successfully configured HashiCorp Vault Client")
.build());
} catch (final Exception e) {
verificationLogger.error("Failed to configure HashiCorp Vault Client", e);
results.add(new ConfigVerificationResult.Builder()
.outcome(ConfigVerificationResult.Outcome.FAILED)
.verificationStepName("Configure HashiCorp Vault Client")
.explanation("Failed to configure HashiCorp Vault Client: " + e.getMessage())
.build());
}
if (service != null) {
try {
service.getServerVersion();
results.add(new ConfigVerificationResult.Builder()
.outcome(ConfigVerificationResult.Outcome.SUCCESSFUL)
.verificationStepName("Connect to HashiCorp Vault Server")
.explanation("Successfully connected to HashiCorp Vault Server")
.build());
} catch (final Exception e) {
verificationLogger.error("Failed to connect to HashiCorp Vault Server", e);
results.add(new ConfigVerificationResult.Builder()
.outcome(ConfigVerificationResult.Outcome.FAILED)
.verificationStepName("Connect to HashiCorp Vault Server")
.explanation("Failed to connect to HashiCorp Vault Server: " + e.getMessage())
.build());
}
}
return results;
}
@OnEnabled
public void onEnabled(final ConfigurationContext context) throws InitializationException {
try {
communicationService = createCommunicationService(context);
} catch (final Exception e) {
throw new InitializationException("Failed to initialize HashiCorp Vault client", e);
}
}
@OnDisabled
public void onDisabled() {
communicationService = null;
}
@Override
public HashiCorpVaultCommunicationService getHashiCorpVaultCommunicationService() {
return communicationService;
}
private HashiCorpVaultCommunicationService createCommunicationService(final ConfigurationContext context) throws IOException {
final List<PropertySource<?>> propertySources = new ArrayList<>();
final String configurationStrategy = context.getProperty(CONFIGURATION_STRATEGY).getValue();
if (DIRECT_PROPERTIES.getValue().equals(configurationStrategy)) {
final PropertySource<?> configurationPropertySource = new DirectPropertySource("Direct Properties", context);
propertySources.add(configurationPropertySource);
} else {
for (final ResourceReference resourceReference : context.getProperty(VAULT_PROPERTIES_FILES).asResources().asList()) {
final String propertiesFile = resourceReference.getLocation();
propertySources.add(HashiCorpVaultConfiguration.createPropertiesFileSource(propertiesFile));
}
}
return new StandardHashiCorpVaultCommunicationService(propertySources.toArray(new PropertySource[0]));
}
static class DirectPropertySource extends PropertySource<ConfigurationContext> {
private static final String VAULT_SSL_KEY_PATTERN = "vault.ssl.(key.*|trust.*|enabledProtocols)";
public DirectPropertySource(final String name, final ConfigurationContext source) {
super(name, source);
}
@Override
public Object getProperty(final String name) {
if (name.matches(VAULT_SSL_KEY_PATTERN)) {
return getSslProperty(name);
}
return getSource().getAllProperties().get(name);
}
private String getSslProperty(final String name) {
if (getSource().getProperty(SSL_CONTEXT_SERVICE).isSet()) {
final SSLContextService sslContextService = getSource().getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class);
switch (name) {
case "vault.ssl.key-store":
return sslContextService.getKeyStoreFile();
case "vault.ssl.key-store-password":
return sslContextService.getKeyStorePassword();
case "vault.ssl.key-store-type":
return sslContextService.getKeyStoreType();
case "vault.ssl.trust-store":
return sslContextService.getTrustStoreFile();
case "vault.ssl.trust-store-password":
return sslContextService.getTrustStorePassword();
case "vault.ssl.trust-store-type":
return sslContextService.getTrustStoreType();
case "vault.ssl.enabledProtocols":
return sslContextService.getSslAlgorithm();
}
}
return null;
}
}
}

View File

@ -0,0 +1,15 @@
# 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.
org.apache.nifi.vault.hashicorp.StandardHashiCorpVaultClientService

View File

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<!--
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.
-->
<head>
<meta charset="utf-8"/>
<title>StandardHashiCorpVaultClientService</title>
<link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css"/>
</head>
<body>
<h1>Configuring the Bootstrap HashiCorp Vault Configuration File</h1>
<p>
The ./conf/bootstrap-hashicorp-vault.conf file that comes with Apache NiFi is a convenient way to configure this
controller service in a manner consistent with the HashiCorpVault sensitive properties provider. Since this file is already used for configuring
the Vault client for protecting sensitive properties in the NiFi configuration files
(see the <a href="../../../../../html/administration-guide.html#hashicorp-vault-providers">Administrator's Guide</a>),
it's a natural starting point for configuring the controller service as well.
</p>
<p>
An example configuration of this properties file is as follows:
</p>
<code>
<pre>
# HTTP or HTTPS URI for HashiCorp Vault is required to enable the Sensitive Properties Provider
vault.uri=https://127.0.0.1:8200
# Optional file supports authentication properties described in the Spring Vault Environment Configuration
# https://docs.spring.io/spring-vault/docs/2.3.x/reference/html/#vault.core.environment-vault-configuration
#
# All authentication properties must be included in bootstrap-hashicorp-vault.conf when this property is not specified.
# Properties in bootstrap-hashicorp-vault.conf take precedence when the same values are defined in both files.
# Token Authentication is the default when the 'vault.authentication' property is not specified.
vault.authentication.properties.file=[full/path/to/vault-auth.properties]
# Optional Timeout properties
vault.connection.timeout=5 secs
vault.read.timeout=15 secs
# Optional TLS properties
vault.ssl.enabledCipherSuites=
vault.ssl.enabledProtocols=TLSv1.3
vault.ssl.key-store=[path/to/keystore.p12]
vault.ssl.key-store-type=PKCS12
vault.ssl.key-store-password=[keystore password]
vault.ssl.trust-store=[path/to/truststore.p12]
vault.ssl.trust-store-type=PKCS12
vault.ssl.trust-store-password=[truststore password]
</pre>
</code>
<p>
In order to use this file in the StandardHashiCorpVaultClientService, specify the following properties:
<ul>
<li><b>Configuration Strategy</b> - Properties Files</li>
<li><b>Vault Properties Files</b> - ./conf/bootstrap-hashicorp-vault.conf</li>
</ul>
</p>
<p>
If your bootstrap configuration includes the vault.authentication.properties.file containing additional authentication properties, this
file will also need to be added to the Vault Properties Files property as a comma-separated value.
</p>
<h3>Configuring the Client using Direct Properties</h3>
<p>
However, if you want to specify or override properties directly in the controller service, you may do this by specifying a Configuration Strategy
of 'Direct Properties'. This can be useful if you are reusing an SSLContextService or want to parameterize the Vault configuration properties.
Authentication-related properties can also be added as sensitive dynamic properties, as seen in the examples below.
</p>
<h3>Vault Authentication</h3>
<p>
Under the hood, the controller service uses Spring Vault, and directly supports the property keys specified in
<a href="https://docs.spring.io/spring-vault/docs/2.3.x/reference/html/#vault.core.environment-vault-configuration">Spring Vault's documentation</a>.
Following are some common examples of authentication with Vault.
</p>
<h4>Token Authentication</h4>
<p>
The simplest authentication scheme uses a rotating token, which is enabled by default in Vault. To specify this mechanism, select "TOKEN" from the
"Vault Authentication" property (the default). However, since the token should rotate by nature, it is a best practice to use the 'Properties Files'
Configuration Strategy, and keep the token value in an external properties file, indicating this filename in the 'Vault Properties Files' property.
Then an external process can rotate the token in the file without updating NiFi configuration. In order to pick up the changed token, the controller
service must be disabled and re-enabled.
</p>
<p>
For testing purposes, however, it may be more convenient to specify the token directly in the controller service. To do so, add a new Sensitive property named
'vault.token' and enter the token as the value.
</p>
<h4>Certificate Authentication</h4>
<p>
Certificate authentication must be enabled in the Vault server before it can be used from NiFi, but it uses the same TLS settings as the actual
client connection, so no additional authentication properties are required. While these TLS settings can be provided in an external properties file,
we will demonstrate configuring an SSLContextService instead.
</p>
<p>
First, create an SSLContextService controller service and configure the Filename, Password, and Type for both the Keystore and Truststore.
Enable it, and assign it as the SSL Context Service in the Vault controller service. Then, simply specify "CERT" as the "Vault Authentication"
property value.
</p>
<h4>Other Authentication Methods</h4>
<p>
To configure the other authentication methods, see the Spring Vault documentation linked above. All relevant properties should be added either
to the external properties files referenced in the "Vault Properties Files" property if using the 'Properties Files' Configuration Strategy,
or added as custom properties with the same name if using the 'Direct Properties' Configuration Strategy.
For example, for the Azure authentication mechanism, properties will have to be added for 'vault.azure-msi.azure-path',
'vault.azure-msi.role', and 'vault.azure-msi.identity-token-service'.
</p>
</body>
</html>

View File

@ -0,0 +1,225 @@
/*
* 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.nifi.vault.hashicorp;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.components.resource.ResourceReference;
import org.apache.nifi.components.resource.ResourceReferences;
import org.apache.nifi.components.resource.ResourceType;
import org.apache.nifi.controller.ConfigurationContext;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.reporting.InitializationException;
import org.apache.nifi.security.util.KeyStoreUtils;
import org.apache.nifi.security.util.StandardTlsConfiguration;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.ssl.SSLContextService;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TestStandardHashiCorpVaultClientService {
private static Path bootstrapHashiCorpVaultConf;
private static TlsConfiguration tlsConfiguration;
private StandardHashiCorpVaultClientService clientService;
private ConfigurationContext mockContext(final Map<PropertyDescriptor, String> properties, final boolean includeSSL) {
final ConfigurationContext context = mock(ConfigurationContext.class);
properties.entrySet().forEach(entry -> mockProperty(context, entry.getKey(), entry.getValue()));
if (properties.containsKey(HashiCorpVaultClientService.VAULT_PROPERTIES_FILES)) {
final PropertyValue propertiesFilesProperty = mock(PropertyValue.class);
final ResourceReferences resources = mock(ResourceReferences.class);
when(resources.asList()).thenReturn(Arrays.asList(properties.get(HashiCorpVaultClientService.VAULT_PROPERTIES_FILES).split(","))
.stream().map(SimpleResourceReference::new)
.collect(Collectors.toList()));
when(propertiesFilesProperty.asResources()).thenReturn(resources);
when(context.getProperty(HashiCorpVaultClientService.VAULT_PROPERTIES_FILES)).thenReturn(propertiesFilesProperty);
}
if (includeSSL) {
final SSLContextService sslContextService = mock(SSLContextService.class);
when(sslContextService.getKeyStoreFile()).thenReturn(tlsConfiguration.getKeystorePath());
when(sslContextService.getKeyStoreType()).thenReturn(tlsConfiguration.getKeystoreType().getType());
when(sslContextService.getKeyStorePassword()).thenReturn(tlsConfiguration.getKeystorePassword());
when(sslContextService.getTrustStoreFile()).thenReturn(tlsConfiguration.getTruststorePath());
when(sslContextService.getTrustStoreType()).thenReturn(tlsConfiguration.getTruststoreType().getType());
when(sslContextService.getTrustStorePassword()).thenReturn(tlsConfiguration.getTruststorePassword());
when(sslContextService.getSslAlgorithm()).thenReturn(tlsConfiguration.getProtocol());
final PropertyValue sslContextServicePropertyValue = mock(PropertyValue.class);
when(sslContextServicePropertyValue.isSet()).thenReturn(true);
when(sslContextServicePropertyValue.asControllerService(SSLContextService.class)).thenReturn(sslContextService);
when(context.getProperty(HashiCorpVaultClientService.SSL_CONTEXT_SERVICE)).thenReturn(sslContextServicePropertyValue);
} else {
final PropertyValue sslContextServicePropertyValue = mock(PropertyValue.class);
when(sslContextServicePropertyValue.isSet()).thenReturn(false);
when(context.getProperty(HashiCorpVaultClientService.SSL_CONTEXT_SERVICE)).thenReturn(sslContextServicePropertyValue);
}
when(context.getAllProperties()).thenReturn(properties.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey().getName(), Map.Entry::getValue)));
return context;
}
private void mockProperty(final ConfigurationContext context, final PropertyDescriptor descriptor, final String value) {
final PropertyValue propertyValue = mock(PropertyValue.class);
when(propertyValue.getValue()).thenReturn(value);
when(context.getProperty(descriptor)).thenReturn(propertyValue);
}
@BeforeAll
public static void initOnce() throws IOException, GeneralSecurityException {
bootstrapHashiCorpVaultConf = Files.createTempFile("bootstrap-hashicorp-vault", "conf");
final File keyStoreFile = File.createTempFile(TestStandardHashiCorpVaultClientService.class.getSimpleName(), ".keystore.p12");
final File trustStoreFile = File.createTempFile(TestStandardHashiCorpVaultClientService.class.getSimpleName(), ".truststore.p12");
final TlsConfiguration requestedTlsConfig = new StandardTlsConfiguration();
tlsConfiguration = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore(requestedTlsConfig, 7, null);
// This should be overridden by any explicit properties
Files.write(bootstrapHashiCorpVaultConf, (String.format("vault.uri=https://localhost:8200\n" +
"vault.authentication=TOKEN\n" +
"vault.token=my-token\n" +
"vault.ssl.key-store=%s\n" +
"vault.ssl.key-store-password=%s\n" +
"vault.ssl.key-store-type=%s\n" +
"vault.ssl.trust-store=%s\n" +
"vault.ssl.trust-store-password=%s\n" +
"vault.ssl.trust-store-type=%s\n",
tlsConfiguration.getKeystorePath().replace("\\", "\\\\"),
tlsConfiguration.getKeystorePassword(),
tlsConfiguration.getKeystoreType().getType(),
tlsConfiguration.getTruststorePath().replace("\\", "\\\\"),
tlsConfiguration.getTruststorePassword(),
tlsConfiguration.getTruststoreType().getType())).getBytes(StandardCharsets.UTF_8));
}
@AfterAll
public static void tearDownOnce() throws IOException {
Files.deleteIfExists(bootstrapHashiCorpVaultConf);
Files.deleteIfExists(Paths.get(tlsConfiguration.getKeystorePath()));
Files.deleteIfExists(Paths.get(tlsConfiguration.getTruststorePath()));
}
@BeforeEach
public void init() {
clientService = new StandardHashiCorpVaultClientService() {
@Override
protected ComponentLog getLogger() {
return mock(ComponentLog.class);
}
};
}
@Test
public void onEnabledHttpDirect() throws InitializationException {
final Map<PropertyDescriptor, String> properties = new HashMap<>();
final PropertyDescriptor vaultToken = new PropertyDescriptor.Builder().name("vault.token").build();
properties.put(HashiCorpVaultClientService.CONFIGURATION_STRATEGY, HashiCorpVaultClientService.DIRECT_PROPERTIES.getValue());
properties.put(HashiCorpVaultClientService.VAULT_URI, "http://localhost:8200");
properties.put(vaultToken, "myToken");
properties.put(HashiCorpVaultClientService.VAULT_AUTHENTICATION, "TOKEN");
final ConfigurationContext context = mockContext(properties, false);
clientService.onEnabled(context);
assertNotNull(clientService.getHashiCorpVaultCommunicationService());
clientService.onDisabled();
assertNull(clientService.getHashiCorpVaultCommunicationService());
}
@Test
public void onEnabledHttpsDirect() throws InitializationException {
final Map<PropertyDescriptor, String> properties = new HashMap<>();
final PropertyDescriptor vaultToken = new PropertyDescriptor.Builder().name("vault.token").build();
properties.put(HashiCorpVaultClientService.CONFIGURATION_STRATEGY, HashiCorpVaultClientService.DIRECT_PROPERTIES.getValue());
properties.put(HashiCorpVaultClientService.VAULT_URI, "https://localhost:8200");
properties.put(vaultToken, "myToken");
properties.put(HashiCorpVaultClientService.VAULT_AUTHENTICATION, "TOKEN");
final ConfigurationContext context = mockContext(properties, true);
clientService.onEnabled(context);
assertNotNull(clientService.getHashiCorpVaultCommunicationService());
clientService.onDisabled();
assertNull(clientService.getHashiCorpVaultCommunicationService());
}
@Test
public void onEnabledHttpsPropertiesFiles() throws InitializationException {
final Map<PropertyDescriptor, String> properties = new HashMap<>();
properties.put(HashiCorpVaultClientService.CONFIGURATION_STRATEGY, HashiCorpVaultClientService.PROPERTIES_FILES.getValue());
properties.put(HashiCorpVaultClientService.VAULT_PROPERTIES_FILES, bootstrapHashiCorpVaultConf.toString());
final ConfigurationContext context = mockContext(properties, true);
clientService.onEnabled(context);
assertNotNull(clientService.getHashiCorpVaultCommunicationService());
clientService.onDisabled();
assertNull(clientService.getHashiCorpVaultCommunicationService());
}
private class SimpleResourceReference implements ResourceReference {
private final String filename;
private SimpleResourceReference(String filename) {
this.filename = filename;
}
@Override
public File asFile() {
return null;
}
@Override
public URL asURL() {
return null;
}
@Override
public InputStream read() throws IOException {
return null;
}
@Override
public boolean isAccessible() {
return false;
}
@Override
public String getLocation() {
return filename;
}
@Override
public ResourceType getResourceType() {
return null;
}
}
}

View File

@ -35,5 +35,27 @@
<artifactId>nifi-hashicorp-vault-parameter-value-provider</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-client-service-api-nar</artifactId>
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-parameter-provider</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-client-service</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-bundle</artifactId>
<version>1.18.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-hashicorp-vault-parameter-provider</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-client-service-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,163 @@
/*
* 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.nifi.vault.hashicorp;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.components.ConfigVerificationResult;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.Validator;
import org.apache.nifi.controller.ConfigurationContext;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.parameter.AbstractParameterProvider;
import org.apache.nifi.parameter.Parameter;
import org.apache.nifi.parameter.ParameterDescriptor;
import org.apache.nifi.parameter.ParameterGroup;
import org.apache.nifi.parameter.ParameterProvider;
import org.apache.nifi.parameter.VerifiableParameterProvider;
import org.apache.nifi.processor.util.StandardValidators;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@CapabilityDescription("Provides parameters from HashiCorp Vault Key/Value Secrets. Each Secret represents a parameter group, " +
"which will map to a Parameter Context. The keys and values in the Secret map to Parameters.")
@Tags({"hashicorp", "vault", "secret"})
public class HashiCorpVaultParameterProvider extends AbstractParameterProvider implements ParameterProvider, VerifiableParameterProvider {
public static final PropertyDescriptor VAULT_CLIENT_SERVICE = new PropertyDescriptor.Builder()
.name("vault-client-service")
.displayName("HashiCorp Vault Client Service")
.description("The service used to interact with HashiCorp Vault")
.identifiesControllerService(HashiCorpVaultClientService.class)
.addValidator(Validator.VALID)
.required(true)
.build();
public static final PropertyDescriptor KV_PATH = new PropertyDescriptor.Builder()
.name("kv-path")
.displayName("Key/Value Path")
.description("The HashiCorp Vault path to the Key/Value Secrets Engine")
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.required(true)
.defaultValue("kv")
.build();
public static final PropertyDescriptor SECRET_NAME_PATTERN = new PropertyDescriptor.Builder()
.name("secret-name-pattern")
.displayName("Secret Name Pattern")
.description("A Regular Expression indicating which Secrets to include as parameter groups to map to Parameter Contexts by name.")
.addValidator(StandardValidators.REGULAR_EXPRESSION_VALIDATOR)
.required(true)
.defaultValue(".*")
.build();
private static final List<PropertyDescriptor> PROPERTIES = Collections.unmodifiableList(Arrays.asList(
VAULT_CLIENT_SERVICE,
KV_PATH,
SECRET_NAME_PATTERN));
private HashiCorpVaultCommunicationService vaultCommunicationService;
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
return PROPERTIES;
}
@Override
public List<ParameterGroup> fetchParameters(final ConfigurationContext context) {
if (vaultCommunicationService == null) {
vaultCommunicationService = getVaultCommunicationService(context);
}
final List<ParameterGroup> parameterGroups = getParameterGroups(vaultCommunicationService, context);
return parameterGroups;
}
private List<ParameterGroup> getParameterGroups(final HashiCorpVaultCommunicationService vaultCommunicationService,
final ConfigurationContext context) {
final String kvPath = context.getProperty(KV_PATH).getValue();
final String secretIncludeRegex = context.getProperty(SECRET_NAME_PATTERN).getValue();
final List<String> allSecretNames = vaultCommunicationService.listKeyValueSecrets(kvPath);
final List<String> secretNames = allSecretNames.stream()
.filter(name -> name.matches(secretIncludeRegex))
.collect(Collectors.toList());
final List<ParameterGroup> parameterGroups = new ArrayList<>();
for (final String secretName : secretNames) {
final Map<String, String> keyValues = vaultCommunicationService.readKeyValueSecretMap(kvPath, secretName);
final List<Parameter> parameters = new ArrayList<>();
keyValues.forEach( (key, value) -> {
final ParameterDescriptor parameterDescriptor = new ParameterDescriptor.Builder().name(key).build();
parameters.add(new Parameter(parameterDescriptor, value, null, true));
});
parameterGroups.add(new ParameterGroup(secretName, parameters));
}
final long parameterCount = parameterGroups.stream()
.flatMap(group -> group.getParameters().stream())
.count();
final List<String> parameterGroupNames = parameterGroups.stream()
.map(group -> group.getGroupName())
.distinct()
.collect(Collectors.toList());
getLogger().info("Fetched parameter groups {}, containing a total of {} parameters", parameterGroupNames, parameterCount);
return parameterGroups;
}
@Override
public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) {
if (VAULT_CLIENT_SERVICE.equals(descriptor)) {
vaultCommunicationService = null;
}
}
@Override
public List<ConfigVerificationResult> verify(final ConfigurationContext context, final ComponentLog verificationLogger) {
final List<ConfigVerificationResult> results = new ArrayList<>();
try {
final HashiCorpVaultCommunicationService vaultCommunicationService = getVaultCommunicationService(context);
final List<ParameterGroup> parameterGroups = getParameterGroups(vaultCommunicationService, context);
final int groupCount = parameterGroups.size();
final long parameterCount = parameterGroups.stream()
.flatMap(group -> group.getParameters().stream())
.count();
results.add(new ConfigVerificationResult.Builder()
.outcome(ConfigVerificationResult.Outcome.SUCCESSFUL)
.verificationStepName("Fetch Secrets as Parameter Groups")
.explanation(String.format("Successfully fetched %s secrets matching the filter as Parameter Groups, " +
"containing a total of %s Parameters.", groupCount, parameterCount))
.build());
} catch (final Exception e) {
verificationLogger.error("Failed to fetch secrets as Parameter Groups", e);
results.add(new ConfigVerificationResult.Builder()
.outcome(ConfigVerificationResult.Outcome.FAILED)
.verificationStepName("Fetch Secrets as Parameter Groups")
.explanation(String.format("Failed to fetch secrets as Parameter Groups: " + e.getMessage()))
.build());
}
return results;
}
HashiCorpVaultCommunicationService getVaultCommunicationService(final ConfigurationContext context) {
final HashiCorpVaultClientService clientService = context.getProperty(VAULT_CLIENT_SERVICE)
.asControllerService(HashiCorpVaultClientService.class);
return clientService.getHashiCorpVaultCommunicationService();
}
}

View File

@ -0,0 +1,15 @@
# 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.
org.apache.nifi.vault.hashicorp.HashiCorpVaultParameterProvider

View File

@ -0,0 +1,162 @@
/*
* 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.nifi.vault.hashicorp;
import org.apache.nifi.components.ConfigVerificationResult;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.controller.ConfigurationContext;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.parameter.Parameter;
import org.apache.nifi.parameter.ParameterDescriptor;
import org.apache.nifi.parameter.ParameterGroup;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TestHashiCorpVaultParameterProvider {
private HashiCorpVaultParameterProvider parameterProvider;
@Mock
private HashiCorpVaultCommunicationService vaultCommunicationService;
private List<ParameterGroup> mockedGroups;
@BeforeEach
public void init() {
vaultCommunicationService = mock(HashiCorpVaultCommunicationService.class);
parameterProvider = new HashiCorpVaultParameterProvider() {
@Override
HashiCorpVaultCommunicationService getVaultCommunicationService(final ConfigurationContext context) {
return vaultCommunicationService;
}
@Override
protected ComponentLog getLogger() {
return mock(ComponentLog.class);
}
};
mockedGroups = new ArrayList<>();
mockedGroups.add(new ParameterGroup("groupA", Arrays.asList(
createParameter("paramA", "valueA"),
createParameter("paramB", "valueB"),
createParameter("otherParam", "valueC"))));
mockedGroups.add(new ParameterGroup("groupA", Arrays.asList(
createParameter("paramC", "valueC"),
createParameter("paramD", "valueD"),
createParameter("otherParam2", "valueE"))));
mockedGroups.add(new ParameterGroup("groupB", Arrays.asList(
createParameter("paramC", "valueC"),
createParameter("paramD", "valueD"),
createParameter("otherParam", "valueE"))));
}
@Test
public void testFetchParameters() {
mockSecrets("kv2", mockedGroups);
final Map<PropertyDescriptor, String> properties = new HashMap<>();
properties.put(HashiCorpVaultParameterProvider.KV_PATH, "kv2");
properties.put(HashiCorpVaultParameterProvider.VAULT_CLIENT_SERVICE, "service");
properties.put(HashiCorpVaultParameterProvider.SECRET_NAME_PATTERN, ".*");
final ConfigurationContext context = mockContext(properties);
final List<ParameterGroup> results = parameterProvider.fetchParameters(context);
assertEquals(3, results.size());
results.forEach(group -> {
assertEquals(3, group.getParameters().size());
});
}
@Test
public void testFetchParametersSecretRegex() {
mockSecrets("kv2", mockedGroups);
final Map<PropertyDescriptor, String> properties = new HashMap<>();
properties.put(HashiCorpVaultParameterProvider.KV_PATH, "kv2");
properties.put(HashiCorpVaultParameterProvider.VAULT_CLIENT_SERVICE, "service");
properties.put(HashiCorpVaultParameterProvider.SECRET_NAME_PATTERN, ".*A");
final ConfigurationContext context = mockContext(properties);
final List<ParameterGroup> results = parameterProvider.fetchParameters(context);
assertEquals(2, results.size());
results.forEach(group -> {
assertEquals(3, group.getParameters().size());
});
}
@Test
public void testVerifyParameters() {
mockSecrets("kv2", mockedGroups);
final Map<PropertyDescriptor, String> properties = new HashMap<>();
properties.put(HashiCorpVaultParameterProvider.KV_PATH, "kv2");
properties.put(HashiCorpVaultParameterProvider.VAULT_CLIENT_SERVICE, "service");
properties.put(HashiCorpVaultParameterProvider.SECRET_NAME_PATTERN, ".*");
final ConfigurationContext context = mockContext(properties);
final List<ConfigVerificationResult> results = parameterProvider.verify(context, mock(ComponentLog.class));
assertEquals(1, results.size());
final String explanation = results.get(0).getExplanation();
assertTrue(explanation.contains("3 secrets"));
assertTrue(explanation.contains("9 Parameters"));
}
private ConfigurationContext mockContext(final Map<PropertyDescriptor, String> properties) {
final ConfigurationContext context = mock(ConfigurationContext.class);
properties.entrySet().forEach(entry -> mockProperty(context, entry.getKey(), entry.getValue()));
return context;
}
private void mockProperty(final ConfigurationContext context, final PropertyDescriptor descriptor, final String value) {
final PropertyValue propertyValue = mock(PropertyValue.class);
lenient().when(propertyValue.getValue()).thenReturn(value);
lenient().when(context.getProperty(descriptor)).thenReturn(propertyValue);
}
private void mockSecrets(final String kvPath, final List<ParameterGroup> parameterGroups) {
when(vaultCommunicationService.listKeyValueSecrets(kvPath))
.thenReturn(parameterGroups.stream().map(group -> group.getGroupName()).collect(Collectors.toList()));
for (final ParameterGroup parameterGroup : parameterGroups) {
final Map<String, String> keyValues = parameterGroup.getParameters().stream()
.collect(Collectors.toMap(parameter -> parameter.getDescriptor().getName(), parameter -> parameter.getValue()));
lenient().when(vaultCommunicationService.readKeyValueSecretMap(kvPath, parameterGroup.getGroupName())).thenReturn(keyValues);
}
}
private Parameter createParameter(final String name, final String value) {
return new Parameter(new ParameterDescriptor.Builder().name(name).build(), value);
}
}

View File

@ -30,7 +30,13 @@
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-vault-utils</artifactId>
<artifactId>nifi-hashicorp-vault-api</artifactId>
<version>1.18.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-hashicorp-vault</artifactId>
<version>1.18.0-SNAPSHOT</version>
</dependency>
<!-- test dependencies -->

View File

@ -25,6 +25,10 @@
<description>A bundle for reading and writing secrets from HashiCorp Vault</description>
<modules>
<module>nifi-hashicorp-vault-parameter-value-provider</module>
<module>nifi-hashicorp-vault-client-service-api</module>
<module>nifi-hashicorp-vault-client-service-api-nar</module>
<module>nifi-hashicorp-vault-client-service</module>
<module>nifi-hashicorp-vault-parameter-provider</module>
<module>nifi-hashicorp-vault-nar</module>
</modules>
</project>