diff --git a/nifi-commons/nifi-vault-utils/pom.xml b/nifi-commons/nifi-vault-utils/pom.xml new file mode 100644 index 0000000000..190388908c --- /dev/null +++ b/nifi-commons/nifi-vault-utils/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + org.apache.nifi + nifi-commons + 1.14.0-SNAPSHOT + + nifi-vault-utils + + + org.springframework.vault + spring-vault-core + 2.3.2 + + + org.springframework + spring-core + 5.3.6 + + + org.apache.nifi + nifi-utils + 1.14.0-SNAPSHOT + + + + com.squareup.okhttp3 + okhttp + 4.9.1 + runtime + + + org.apache.nifi + nifi-security-utils-api + 1.14.0-SNAPSHOT + test + + + org.apache.nifi + nifi-security-utils + 1.14.0-SNAPSHOT + test + + + + diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/HashiCorpVaultCommunicationService.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/HashiCorpVaultCommunicationService.java new file mode 100644 index 0000000000..977b369e54 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/HashiCorpVaultCommunicationService.java @@ -0,0 +1,46 @@ +/* + * 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; + +/** + * A service to handle all communication with an instance of HashiCorp Vault. + * @see https://www.vaultproject.io/ + */ +public interface HashiCorpVaultCommunicationService { + + /** + * Encrypts the given plaintext using Vault's Transit Secrets Engine. + * + * @see https://www.vaultproject.io/api-docs/secret/transit + * @param transitKey A named encryption key used in the Transit Secrets Engine. The key is expected to have + * already been configured in the Vault instance. + * @param plainText The plaintext to encrypt + * @return The cipher text + */ + String encrypt(String transitKey, byte[] plainText); + + /** + * Decrypts the given cipher text using Vault's Transit Secrets Engine. + * + * @see https://www.vaultproject.io/api-docs/secret/transit + * @param transitKey A named encryption key used in the Transit Secrets Engine. The key is expected to have + * already been configured in the Vault instance. + * @param cipherText The cipher text to decrypt + * @return The decrypted plaintext + */ + byte[] decrypt(String transitKey, String cipherText); +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/HashiCorpVaultConfigurationException.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/HashiCorpVaultConfigurationException.java new file mode 100644 index 0000000000..8948d433e6 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/HashiCorpVaultConfigurationException.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Indicates a misconfiguration of the Vault client. + */ +public class HashiCorpVaultConfigurationException extends RuntimeException { + public HashiCorpVaultConfigurationException() { + } + + public HashiCorpVaultConfigurationException(String message) { + super(message); + } + + public HashiCorpVaultConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/StandardHashiCorpVaultCommunicationService.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/StandardHashiCorpVaultCommunicationService.java new file mode 100644 index 0000000000..8f92fb30cc --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/StandardHashiCorpVaultCommunicationService.java @@ -0,0 +1,94 @@ +/* + * 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.util.FormatUtils; +import org.apache.nifi.vault.hashicorp.config.HashiCorpVaultConfiguration; +import org.apache.nifi.vault.hashicorp.config.HashiCorpVaultProperties; +import org.springframework.vault.authentication.SimpleSessionManager; +import org.springframework.vault.client.ClientHttpRequestFactoryFactory; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.core.VaultTransitOperations; +import org.springframework.vault.support.Ciphertext; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.Plaintext; +import org.springframework.vault.support.SslConfiguration; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * Implements the VaultCommunicationService using Spring Vault + */ +public class StandardHashiCorpVaultCommunicationService implements HashiCorpVaultCommunicationService { + private static final String HTTPS = "https"; + + private final HashiCorpVaultConfiguration vaultConfiguration; + private final VaultTemplate vaultTemplate; + private final VaultTransitOperations transitOperations; + + /** + * Creates a VaultCommunicationService that uses Spring Vault. + * @param vaultProperties Properties to configure the service + * @throws HashiCorpVaultConfigurationException If the configuration was invalid + */ + public StandardHashiCorpVaultCommunicationService(final HashiCorpVaultProperties vaultProperties) throws HashiCorpVaultConfigurationException { + this.vaultConfiguration = new HashiCorpVaultConfiguration(vaultProperties); + + final SslConfiguration sslConfiguration = vaultProperties.getUri().contains(HTTPS) + ? vaultConfiguration.sslConfiguration() : SslConfiguration.unconfigured(); + + final ClientOptions clientOptions = getClientOptions(vaultProperties); + + vaultTemplate = new VaultTemplate(vaultConfiguration.vaultEndpoint(), + ClientHttpRequestFactoryFactory.create(clientOptions, sslConfiguration), + new SimpleSessionManager(vaultConfiguration.clientAuthentication())); + + transitOperations = vaultTemplate.opsForTransit(); + } + + private static ClientOptions getClientOptions(HashiCorpVaultProperties vaultProperties) { + final ClientOptions clientOptions = new ClientOptions(); + Duration readTimeoutDuration = clientOptions.getReadTimeout(); + Duration connectionTimeoutDuration = clientOptions.getConnectionTimeout(); + final Optional configuredReadTimeout = vaultProperties.getReadTimeout(); + if (configuredReadTimeout.isPresent()) { + readTimeoutDuration = getDuration(configuredReadTimeout.get()); + } + final Optional configuredConnectionTimeout = vaultProperties.getConnectionTimeout(); + if (configuredConnectionTimeout.isPresent()) { + connectionTimeoutDuration = getDuration(configuredConnectionTimeout.get()); + } + return new ClientOptions(connectionTimeoutDuration, readTimeoutDuration); + } + + private static Duration getDuration(String formattedDuration) { + final double duration = FormatUtils.getPreciseTimeDuration(formattedDuration, TimeUnit.MILLISECONDS); + return Duration.ofMillis(Double.valueOf(duration).longValue()); + } + + @Override + public String encrypt(final String transitKey, final byte[] plainText) { + return transitOperations.encrypt(transitKey, Plaintext.of(plainText)).getCiphertext(); + } + + @Override + public byte[] decrypt(final String transitKey, final String cipherText) { + return transitOperations.decrypt(transitKey, Ciphertext.of(cipherText)).getPlaintext(); + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultApplicationContext.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultApplicationContext.java new file mode 100644 index 0000000000..1d92f1c380 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultApplicationContext.java @@ -0,0 +1,39 @@ +/* + * 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.config; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +import java.nio.file.Paths; + +/** + * Basic ApplicationContext that defines resources as FileSystemResource objects. + */ +public class HashiCorpVaultApplicationContext extends StaticApplicationContext { + + public HashiCorpVaultApplicationContext(ConfigurableEnvironment env) { + this.setEnvironment(env); + } + + @Override + public Resource getResource(String location) { + return new FileSystemResource(Paths.get(location)); + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultConfiguration.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultConfiguration.java new file mode 100644 index 0000000000..44ad4e6ef7 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.config; + +import org.apache.nifi.vault.hashicorp.HashiCorpVaultConfigurationException; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.support.ResourcePropertySource; +import org.springframework.vault.config.EnvironmentVaultConfiguration; + +import java.io.IOException; +import java.nio.file.Paths; + +/** + * A Vault configuration that uses the NiFiVaultEnvironment. + */ +public class HashiCorpVaultConfiguration extends EnvironmentVaultConfiguration { + + public HashiCorpVaultConfiguration(final HashiCorpVaultProperties vaultProperties) throws HashiCorpVaultConfigurationException { + final ConfigurableEnvironment env = new StandardEnvironment(); + + try { + env.getPropertySources().addFirst(new ResourcePropertySource(new FileSystemResource(Paths.get(vaultProperties.getAuthPropertiesFilename())))); + } catch (IOException e) { + throw new HashiCorpVaultConfigurationException("Could not load auth properties", e); + } + env.getPropertySources().addFirst(new HashiCorpVaultPropertySource(vaultProperties)); + + this.setApplicationContext(new HashiCorpVaultApplicationContext(env)); + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultProperties.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultProperties.java new file mode 100644 index 0000000000..867ca0c494 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultProperties.java @@ -0,0 +1,246 @@ +/* + * 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.config; + +import org.apache.nifi.vault.hashicorp.HashiCorpVaultConfigurationException; + +import java.io.File; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.Optional; + +/** + * Properties to configure the HashiCorpVaultCommunicationService. The only properties considered mandatory are uri and + * authPropertiesFilename. See the following link for valid vault authentication properties (default is + * vault.authentication=TOKEN, expecting a vault.token property to be supplied). + * + * @see + * https://docs.spring.io/spring-vault/docs/2.3.1/reference/html/#vault.core.environment-vault-configuration + */ +public class HashiCorpVaultProperties { + public static final String HTTPS = "https"; + private final String uri; + private final String authPropertiesFilename; + private final HashiCorpVaultSslProperties ssl; + private final Optional connectionTimeout; + private final Optional readTimeout; + + private HashiCorpVaultProperties(final String uri, String keyStore, final String keyStoreType, final String keyStorePassword, final String trustStore, + final String trustStoreType, final String trustStorePassword, final String authPropertiesFilename, + final String enabledTlsCipherSuites, final String enabledTlsProtocols, final String connectionTimeout, final String readTimeout) { + Objects.requireNonNull(uri, "Vault URI is required"); + Objects.requireNonNull(authPropertiesFilename, "Vault auth properties filename is required"); + this.uri = uri; + this.authPropertiesFilename = authPropertiesFilename; + this.ssl = new HashiCorpVaultSslProperties(keyStore, keyStoreType, keyStorePassword, trustStore, trustStoreType, trustStorePassword, + enabledTlsCipherSuites, enabledTlsProtocols); + this.connectionTimeout = connectionTimeout == null ? Optional.empty() : Optional.of(connectionTimeout); + this.readTimeout = readTimeout == null ? Optional.empty() : Optional.of(readTimeout); + + if (uri.startsWith(HTTPS)) { + Objects.requireNonNull(keyStore, "KeyStore is required with an https URI"); + Objects.requireNonNull(keyStorePassword, "KeyStore password is required with an https URI"); + Objects.requireNonNull(keyStoreType, "KeyStore type is required with an https URI"); + Objects.requireNonNull(trustStore, "TrustStore is required with an https URI"); + Objects.requireNonNull(trustStorePassword, "TrustStore password is required with an https URI"); + Objects.requireNonNull(trustStoreType, "TrustStore type is required with an https URI"); + } + validateAuthProperties(); + } + + private void validateAuthProperties() throws HashiCorpVaultConfigurationException { + final File authPropertiesFile = Paths.get(authPropertiesFilename).toFile(); + if (!authPropertiesFile.exists()) { + throw new HashiCorpVaultConfigurationException(String.format("Auth properties file [%s] does not exist", authPropertiesFilename)); + } + } + + @HashiCorpVaultProperty + public String getUri() { + return uri; + } + + @HashiCorpVaultProperty + public HashiCorpVaultSslProperties getSsl() { + return ssl; + } + + public String getAuthPropertiesFilename() { + return authPropertiesFilename; + } + + public Optional getConnectionTimeout() { + return connectionTimeout; + } + + public Optional getReadTimeout() { + return readTimeout; + } + + /** + * Builder for HashiCorpVaultProperties. The only properties that are considered mandatory are uri and authPropertiesFilename. + */ + public static class HashiCorpVaultPropertiesBuilder { + private String uri; + private String keyStore; + private String keyStoreType; + private String keyStorePassword; + private String trustStore; + private String trustStoreType; + private String trustStorePassword; + private String authPropertiesFilename; + private String enabledTlsCipherSuites; + private String enabledTlsProtocols; + private String connectionTimeout; + private String readTimeout; + + /** + * Set the Vault URI (e.g., http://localhost:8200). If using https protocol, the KeyStore and TrustStore + * properties are expected to also be set. + * @param uri Vault's URI + * @return + */ + public HashiCorpVaultPropertiesBuilder setUri(String uri) { + this.uri = uri; + return this; + } + + /** + * Sets the path to the keyStore. + * @param keyStore Path to the keyStore + * @return + */ + public HashiCorpVaultPropertiesBuilder setKeyStore(String keyStore) { + this.keyStore = keyStore; + return this; + } + + /** + * Sets keyStore type (e.g., JKS, PKCS12). + * @param keyStoreType KeyStore type + * @return + */ + public HashiCorpVaultPropertiesBuilder setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + return this; + } + + /** + * Sets the keyStore password. + * @param keyStorePassword KeyStore password + * @return + */ + public HashiCorpVaultPropertiesBuilder setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + return this; + } + + /** + * Sets the path to the trustStore. + * @param trustStore Path to the trustStore + * @return + */ + public HashiCorpVaultPropertiesBuilder setTrustStore(String trustStore) { + this.trustStore = trustStore; + return this; + } + + /** + * Sets the trustStore type (e.g., JKS, PKCS12). + * @param trustStoreType TrustStore type + * @return + */ + public HashiCorpVaultPropertiesBuilder setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + return this; + } + + /** + * Sets the trustStore passsword. + * @param trustStorePassword TrustStore password + * @return + */ + public HashiCorpVaultPropertiesBuilder setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + return this; + } + + /** + * Sets the path to the vault authentication properties file. See the following link for valid + * vault authentication properties (default is vault.authentication=TOKEN, expecting a vault.token property + * to be supplied). + * @see + * https://docs.spring.io/spring-vault/docs/2.3.1/reference/html/#vault.core.environment-vault-configuration + * @param authPropertiesFilename The filename of a properties file containing Spring Vault authentication + * properties + * @return + */ + public HashiCorpVaultPropertiesBuilder setAuthPropertiesFilename(String authPropertiesFilename) { + this.authPropertiesFilename = authPropertiesFilename; + return this; + } + + /** + * Sets an optional comma-separated list of enabled TLS cipher suites. + * @param enabledTlsCipherSuites Enabled TLS cipher suites (only these will be enabled) + * @return + */ + public HashiCorpVaultPropertiesBuilder setEnabledTlsCipherSuites(String enabledTlsCipherSuites) { + this.enabledTlsCipherSuites = enabledTlsCipherSuites; + return this; + } + + /** + * Sets an optional comma-separated list of enabled TLS protocols. + * @param enabledTlsProtocols Enabled TLS protocols (only these will be enabled) + * @return + */ + public HashiCorpVaultPropertiesBuilder setEnabledTlsProtocols(String enabledTlsProtocols) { + this.enabledTlsProtocols = enabledTlsProtocols; + return this; + } + + /** + * Sets the connection timeout for the HTTP client, using the standard NiFi duration format (e.g., 5 secs) + * @param connectionTimeout Connection timeout (default is 5 secs) + * @return + */ + public HashiCorpVaultPropertiesBuilder setConnectionTimeout(String connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * Sets the read timeout for the HTTP client, using the standard NiFi duration format (e.g., 15 secs). + * @param readTimeout Read timeout (default is 15 secs) + * @return + */ + public HashiCorpVaultPropertiesBuilder setReadTimeout(String readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + /** + * Build the VaultProperties. + * @return + */ + public HashiCorpVaultProperties build() { + return new HashiCorpVaultProperties(uri, keyStore, keyStoreType, keyStorePassword, trustStore, trustStoreType, + trustStorePassword, authPropertiesFilename, enabledTlsCipherSuites, enabledTlsProtocols, connectionTimeout, readTimeout); + } + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultProperty.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultProperty.java new file mode 100644 index 0000000000..81bd87ec99 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultProperty.java @@ -0,0 +1,31 @@ +/* + * 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.config; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a vault property that should be mapped to a Spring Vault property key. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface HashiCorpVaultProperty { +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultPropertySource.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultPropertySource.java new file mode 100644 index 0000000000..446efc121c --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultPropertySource.java @@ -0,0 +1,60 @@ +/* + * 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.config; + +import org.apache.nifi.vault.hashicorp.config.lookup.BeanPropertyLookup; +import org.apache.nifi.vault.hashicorp.config.lookup.PropertyLookup; +import org.springframework.core.env.PropertySource; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HashiCorpVaultPropertySource extends PropertySource { + private static final String PREFIX = "vault"; + private static final Pattern DASH_LETTER_PATTERN = Pattern.compile("-[a-z]"); + + private PropertyLookup propertyLookup; + + public HashiCorpVaultPropertySource(HashiCorpVaultProperties source) { + super(HashiCorpVaultPropertySource.class.getName(), source); + + propertyLookup = new BeanPropertyLookup(PREFIX, HashiCorpVaultProperties.class, HashiCorpVaultProperty.class); + } + + @Override + public Object getProperty(final String key) { + Objects.requireNonNull(key, "Property key cannot be null"); + + return propertyLookup.getPropertyValue(getPropertyKey(key), getSource()); + } + + /** + * Converts key names from format test-value to testValue + * @param springPropertyKey A Spring Vault property key + * @return + */ + private String getPropertyKey(String springPropertyKey) { + final Matcher m = DASH_LETTER_PATTERN.matcher(springPropertyKey); + final StringBuffer result = new StringBuffer(); + while (m.find()) { + m.appendReplacement(result, m.group(0).substring(1).toUpperCase()); + } + m.appendTail(result); + return result.toString(); + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultSslProperties.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultSslProperties.java new file mode 100644 index 0000000000..99d63d53b1 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/HashiCorpVaultSslProperties.java @@ -0,0 +1,81 @@ +/* + * 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.config; + +public class HashiCorpVaultSslProperties { + private final String keyStore; + private final String keyStoreType; + private final String keyStorePassword; + private final String trustStore; + private final String trustStoreType; + private final String trustStorePassword; + private final String enabledCipherSuites; + private final String enabledProtocols; + + public HashiCorpVaultSslProperties(String keyStore, String keyStoreType, String keyStorePassword, String trustStore, + String trustStoreType, String trustStorePassword, + String enabledCipherSuites, String enabledProtocols) { + this.keyStore = keyStore; + this.keyStoreType = keyStoreType; + this.keyStorePassword = keyStorePassword; + this.trustStore = trustStore; + this.trustStoreType = trustStoreType; + this.trustStorePassword = trustStorePassword; + this.enabledCipherSuites = enabledCipherSuites; + this.enabledProtocols = enabledProtocols; + } + + @HashiCorpVaultProperty + public String getKeyStore() { + return keyStore; + } + + @HashiCorpVaultProperty + public String getKeyStoreType() { + return keyStoreType; + } + + @HashiCorpVaultProperty + public String getKeyStorePassword() { + return keyStorePassword; + } + + @HashiCorpVaultProperty + public String getTrustStore() { + return trustStore; + } + + @HashiCorpVaultProperty + public String getTrustStoreType() { + return trustStoreType; + } + + @HashiCorpVaultProperty + public String getTrustStorePassword() { + return trustStorePassword; + } + + @HashiCorpVaultProperty + public String getEnabledCipherSuites() { + return enabledCipherSuites; + } + + @HashiCorpVaultProperty + public String getEnabledProtocols() { + return enabledProtocols; + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/BeanPropertyLookup.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/BeanPropertyLookup.java new file mode 100644 index 0000000000..2ad1ac101c --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/BeanPropertyLookup.java @@ -0,0 +1,81 @@ +/* + * 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.config.lookup; + +import org.apache.nifi.vault.hashicorp.HashiCorpVaultConfigurationException; +import org.springframework.beans.BeanUtils; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A property lookup that indexes the properties of a Java bean. + */ +public class BeanPropertyLookup extends PropertyLookup { + private static final String SEPARATOR = "."; + + private final Map propertyLookupMap; + + public BeanPropertyLookup(final String prefix, final Class beanClass, final Class propertyFilter) { + this(prefix, beanClass, propertyFilter, null); + } + + private BeanPropertyLookup(final String prefix, final Class beanClass, final Class propertyFilter, + final PropertyDescriptor propertyDescriptor) { + super(propertyDescriptor); + propertyLookupMap = Arrays.stream(BeanUtils.getPropertyDescriptors(beanClass)) + .filter(pd -> pd.getReadMethod().getAnnotation(propertyFilter) != null) + .collect(Collectors.toMap( + pd -> getPropertyKey(prefix, pd), + pd -> pd.getReadMethod().getReturnType().equals(String.class) ? new ValuePropertyLookup(pd) + : new BeanPropertyLookup(getPropertyKey(prefix, pd), pd.getReadMethod().getReturnType(), propertyFilter, pd) + )); + } + + private static String getPropertyKey(final String prefix, final PropertyDescriptor propertyDescriptor) { + return prefix == null ? propertyDescriptor.getDisplayName() : String.join(SEPARATOR, prefix, propertyDescriptor.getDisplayName()); + } + + @Override + public Object getPropertyValue(final String propertyKey, final Object obj) { + if (propertyLookupMap.containsKey(propertyKey)) { + final PropertyLookup propertyLookup = propertyLookupMap.get(propertyKey); + return propertyLookup.getPropertyValue(propertyKey, propertyLookup.getEnclosingObject(obj)); + } + for(final Map.Entry entry : propertyLookupMap.entrySet()) { + final String key = entry.getKey(); + if (propertyKey.startsWith(key + SEPARATOR)) { + final PropertyLookup propertyLookup = entry.getValue(); + return propertyLookup.getPropertyValue(propertyKey, propertyLookup.getEnclosingObject(obj)); + } + } + return null; + } + + @Override + public Object getEnclosingObject(Object obj) { + try { + return getPropertyDescriptor().getReadMethod().invoke(obj); + } catch (final IllegalAccessException | InvocationTargetException e) { + throw new HashiCorpVaultConfigurationException("Could not invoke " + getPropertyDescriptor().getDisplayName()); + } + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/PropertyLookup.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/PropertyLookup.java new file mode 100644 index 0000000000..a2cd8013e6 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/PropertyLookup.java @@ -0,0 +1,50 @@ +/* + * 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.config.lookup; + +import java.beans.PropertyDescriptor; + +/** + * Provides a method of looking up property values. + */ +public abstract class PropertyLookup { + + private final PropertyDescriptor propertyDescriptor; + + protected PropertyLookup(final PropertyDescriptor propertyDescriptor) { + this.propertyDescriptor = propertyDescriptor; + } + + /** + * Returns the value of a property. + * @param propertyKey The property key (e.g., object.child.propertyValue) + * @param obj The object from which to retrieve the property. + * @return The property value + */ + public abstract Object getPropertyValue(final String propertyKey, final Object obj); + + /** + * Returns the enclosing object of the property. + * @param obj The top level object + * @return The appropriate object that contains the property + */ + public abstract Object getEnclosingObject(final Object obj); + + protected PropertyDescriptor getPropertyDescriptor() { + return propertyDescriptor; + } +} diff --git a/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/ValuePropertyLookup.java b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/ValuePropertyLookup.java new file mode 100644 index 0000000000..4a1e744061 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/main/java/org/apache/nifi/vault/hashicorp/config/lookup/ValuePropertyLookup.java @@ -0,0 +1,46 @@ +/* + * 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.config.lookup; + +import org.apache.nifi.vault.hashicorp.HashiCorpVaultConfigurationException; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; + +/** + * A simple property lookup that invokes the property descriptor to retrieve the value of the property. + */ +public class ValuePropertyLookup extends PropertyLookup { + + public ValuePropertyLookup(final PropertyDescriptor propertyDescriptor) { + super(propertyDescriptor); + } + + @Override + public Object getPropertyValue(final String propertyKey, final Object obj) { + try { + return getPropertyDescriptor().getReadMethod().invoke(obj); + } catch (final IllegalAccessException | InvocationTargetException e) { + throw new HashiCorpVaultConfigurationException("Could not get the value of " + propertyKey); + } + } + + @Override + public Object getEnclosingObject(Object obj) { + return obj; + } +} diff --git a/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/StandardHashiCorpVaultCommunicationServiceIT.java b/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/StandardHashiCorpVaultCommunicationServiceIT.java new file mode 100644 index 0000000000..60d64a9168 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/StandardHashiCorpVaultCommunicationServiceIT.java @@ -0,0 +1,67 @@ +/* + * 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.vault.hashicorp.config.HashiCorpVaultProperties; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +/** + * The simplest way to run this test is by installing Vault locally, then running: + * + * vault server -dev + * vault secrets enable transit + * vault write -f transit/keys/nifi + * + * Make note of the Root Token and create a properties file with the contents: + * vault.token=[Root Token] + * + * Then, set the system property -Dvault.auth.properties to the file path of the above properties file when + * running the integration test. + */ +public class StandardHashiCorpVaultCommunicationServiceIT { + + private static final String TRANSIT_KEY = "nifi"; + + private HashiCorpVaultCommunicationService vcs; + + @Before + public void init() { + vcs = new StandardHashiCorpVaultCommunicationService(new HashiCorpVaultProperties.HashiCorpVaultPropertiesBuilder() + .setAuthPropertiesFilename(System.getProperty("vault.auth.properties")) + .setUri("http://127.0.0.1:8200") + .build()); + } + + @Test + public void testEncryptDecrypt() { + this.runEncryptDecryptTest(); + } + + public void runEncryptDecryptTest() { + String plaintext = "this is the plaintext"; + + String ciphertext = vcs.encrypt(TRANSIT_KEY, plaintext.getBytes(StandardCharsets.UTF_8)); + + byte[] decrypted = vcs.decrypt(TRANSIT_KEY, ciphertext); + + Assert.assertEquals(plaintext, new String(decrypted, StandardCharsets.UTF_8)); + } +} diff --git a/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/TestHashiCorpVaultConfiguration.java b/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/TestHashiCorpVaultConfiguration.java new file mode 100644 index 0000000000..38cb2d9484 --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/TestHashiCorpVaultConfiguration.java @@ -0,0 +1,195 @@ +/* + * 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.vault.hashicorp.config.HashiCorpVaultConfiguration; +import org.apache.nifi.vault.hashicorp.config.HashiCorpVaultProperties; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.support.SslConfiguration; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class TestHashiCorpVaultConfiguration { + public static final String VAULT_AUTHENTICATION = "vault.authentication"; + public static final String VAULT_TOKEN = "vault.token"; + + private static final String TEST_TOKEN_VALUE = "test-token"; + private static final String TOKEN_VALUE = "TOKEN"; + private static final String URI_VALUE = "http://localhost:8200"; + private static final String KEYSTORE_PASSWORD_VALUE = "keystorePassword"; + private static final String KEYSTORE_TYPE_VALUE = "keystoreType"; + private static final String TRUSTSTORE_PASSWORD_VALUE = "truststorePassword"; + private static final String TRUSTSTORE_TYPE_VALUE = "truststoreType"; + public static final String TLS_V_1_3_VALUE = "TLSv1.3"; + public static final String TEST_CIPHER_SUITE_VALUE = "Test cipher suite"; + + private static Path keystoreFile; + private static Path truststoreFile; + + private HashiCorpVaultProperties.HashiCorpVaultPropertiesBuilder propertiesBuilder; + private static File authProps; + + private HashiCorpVaultConfiguration config; + + @BeforeClass + public static void initClass() throws IOException { + keystoreFile = Files.createTempFile("test", ".jks"); + truststoreFile = Files.createTempFile("test", ".jks"); + authProps = writeBasicVaultAuthProperties(); + } + + @AfterClass + public static void cleanUpClass() throws IOException { + Files.deleteIfExists(keystoreFile); + Files.deleteIfExists(truststoreFile); + Files.deleteIfExists(authProps.toPath()); + } + + @Before + public void init() throws IOException { + propertiesBuilder = new HashiCorpVaultProperties.HashiCorpVaultPropertiesBuilder() + .setUri(URI_VALUE) + .setAuthPropertiesFilename(authProps.getAbsolutePath()); + + } + + public static File writeVaultAuthProperties(final Map properties) throws IOException { + File authProps = File.createTempFile("vault-", ".properties"); + writeProperties(properties, authProps); + return authProps; + } + + /** + * Writes a new temp vault authentication properties file with the following properties: + * vault.authentication=TOKEN + * vault.token=test-token + * @return The created temp file + * @throws IOException If the file could not be written + */ + public static File writeBasicVaultAuthProperties() throws IOException { + Map properties = new HashMap<>(); + properties.put(VAULT_AUTHENTICATION, TOKEN_VALUE); + properties.put(VAULT_TOKEN, TEST_TOKEN_VALUE); + return writeVaultAuthProperties(properties); + } + + public static void writeProperties(Map props, File authProps) throws IOException { + Properties properties = new Properties(); + + for (Map.Entry entry : props.entrySet()) { + properties.put(entry.getKey(), entry.getValue()); + } + try (Writer writer = new FileWriter(authProps)) { + properties.store(writer, "Vault test authentication properties"); + } + } + + public void runTest() { + config = new HashiCorpVaultConfiguration(propertiesBuilder.build()); + + VaultEndpoint endpoint = config.vaultEndpoint(); + Assert.assertEquals("localhost", endpoint.getHost()); + Assert.assertEquals(8200, endpoint.getPort()); + Assert.assertEquals("http", endpoint.getScheme()); + + ClientAuthentication clientAuthentication = config.clientAuthentication(); + Assert.assertNotNull(clientAuthentication); + } + + @Test + public void testBasicProperties() { + this.runTest(); + } + + @Test + public void testTlsProperties() throws IOException { + propertiesBuilder.setKeyStore(keystoreFile.toFile().getAbsolutePath()); + propertiesBuilder.setKeyStorePassword(KEYSTORE_PASSWORD_VALUE); + propertiesBuilder.setKeyStoreType(KEYSTORE_TYPE_VALUE); + propertiesBuilder.setTrustStore(truststoreFile.toFile().getAbsolutePath()); + propertiesBuilder.setTrustStorePassword(TRUSTSTORE_PASSWORD_VALUE); + propertiesBuilder.setTrustStoreType(TRUSTSTORE_TYPE_VALUE); + propertiesBuilder.setEnabledTlsProtocols(TLS_V_1_3_VALUE); + propertiesBuilder.setEnabledTlsCipherSuites(TEST_CIPHER_SUITE_VALUE); + + this.runTest(); + + SslConfiguration sslConfiguration = config.sslConfiguration(); + Assert.assertEquals(keystoreFile.toFile().getAbsolutePath(), sslConfiguration.getKeyStoreConfiguration().getResource().getFile().getAbsolutePath()); + Assert.assertEquals(KEYSTORE_PASSWORD_VALUE, new String(sslConfiguration.getKeyStoreConfiguration().getStorePassword())); + Assert.assertEquals(KEYSTORE_TYPE_VALUE, sslConfiguration.getKeyStoreConfiguration().getStoreType()); + Assert.assertEquals(truststoreFile.toFile().getAbsolutePath(), sslConfiguration.getTrustStoreConfiguration().getResource().getFile().getAbsolutePath()); + Assert.assertEquals(TRUSTSTORE_PASSWORD_VALUE, new String(sslConfiguration.getTrustStoreConfiguration().getStorePassword())); + Assert.assertEquals(TRUSTSTORE_TYPE_VALUE, sslConfiguration.getTrustStoreConfiguration().getStoreType()); + Assert.assertEquals(Arrays.asList(TLS_V_1_3_VALUE), sslConfiguration.getEnabledProtocols()); + Assert.assertEquals(Arrays.asList(TEST_CIPHER_SUITE_VALUE), sslConfiguration.getEnabledCipherSuites()); + } + + @Test + public void testInvalidTLS() { + propertiesBuilder.setUri(URI_VALUE.replace("http", "https")); + Assert.assertThrows(NullPointerException.class, () -> this.runTest()); + } + + @Test + public void testMissingAuthToken() throws IOException { + File authProperties = null; + try { + final Map props = new HashMap<>(); + props.put(VAULT_AUTHENTICATION, TOKEN_VALUE); + authProperties = writeVaultAuthProperties(props); + propertiesBuilder.setAuthPropertiesFilename(authProperties.getAbsolutePath()); + + Assert.assertThrows(IllegalArgumentException.class, () -> this.runTest()); + } finally { + if (authProperties != null) { + Files.deleteIfExists(authProperties.toPath()); + } + } + } + + @Test + public void testMissingAuthType() throws IOException { + File authProperties = null; + try { + final Map props = new HashMap<>(); + authProperties = writeVaultAuthProperties(props); + propertiesBuilder.setAuthPropertiesFilename(authProperties.getAbsolutePath()); + + Assert.assertThrows(IllegalArgumentException.class, () -> this.runTest()); + } finally { + if (authProperties != null) { + Files.deleteIfExists(authProperties.toPath()); + } + } + } +} diff --git a/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/TestStandardHashiCorpVaultCommunicationService.java b/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/TestStandardHashiCorpVaultCommunicationService.java new file mode 100644 index 0000000000..f9b102a81a --- /dev/null +++ b/nifi-commons/nifi-vault-utils/src/test/java/org/apache/nifi/vault/hashicorp/TestStandardHashiCorpVaultCommunicationService.java @@ -0,0 +1,125 @@ +/* + * 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.security.util.KeyStoreUtils; +import org.apache.nifi.security.util.TlsConfiguration; +import org.apache.nifi.vault.hashicorp.config.HashiCorpVaultProperties; +import org.apache.nifi.vault.hashicorp.config.HashiCorpVaultSslProperties; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; + +public class TestStandardHashiCorpVaultCommunicationService { + public static final String URI_VALUE = "http://127.0.0.1:8200"; + public static final String CIPHER_SUITE_VALUE = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"; + + private HashiCorpVaultProperties properties; + private HashiCorpVaultSslProperties sslProperties; + private File authProps; + + @Before + public void init() throws IOException { + authProps = TestHashiCorpVaultConfiguration.writeBasicVaultAuthProperties(); + + properties = Mockito.mock(HashiCorpVaultProperties.class); + sslProperties = Mockito.mock(HashiCorpVaultSslProperties.class); + + Mockito.when(properties.getUri()).thenReturn(URI_VALUE); + Mockito.when(properties.getAuthPropertiesFilename()).thenReturn(authProps.getAbsolutePath()); + Mockito.when(properties.getSsl()).thenReturn(sslProperties); + } + + @After + public void cleanUp() throws IOException { + Files.deleteIfExists(authProps.toPath()); + } + + private HashiCorpVaultCommunicationService configureService() { + return new StandardHashiCorpVaultCommunicationService(properties); + } + + @Test + 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 each to check if they are configured + Mockito.verify(properties, Mockito.times(1)).getConnectionTimeout(); + Mockito.verify(properties, Mockito.times(1)).getReadTimeout(); + + // These should not be called because TLS is not configured + this.ensureTlsPropertiesAccessed(0); + } + + private void ensureTlsPropertiesAccessed(int numberOfTimes) { + Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getKeyStore(); + Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getKeyStoreType(); + Mockito.verify(sslProperties, Mockito.times(numberOfTimes)).getKeyStorePassword(); + 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 + public void testTimeouts() { + Mockito.when(properties.getConnectionTimeout()).thenReturn(Optional.of("20 secs")); + Mockito.when(properties.getReadTimeout()).thenReturn(Optional.of("40 secs")); + this.configureService(); + + Mockito.verify(properties, Mockito.times(1)).getConnectionTimeout(); + Mockito.verify(properties, Mockito.times(1)).getReadTimeout(); + } + + @Test + public void testTLS() throws GeneralSecurityException, IOException { + TlsConfiguration tlsConfiguration = KeyStoreUtils.createTlsConfigAndNewKeystoreTruststore(); + try { + Mockito.when(sslProperties.getKeyStore()).thenReturn(tlsConfiguration.getKeystorePath()); + Mockito.when(sslProperties.getKeyStorePassword()).thenReturn(tlsConfiguration.getKeystorePassword()); + Mockito.when(sslProperties.getKeyStoreType()).thenReturn(tlsConfiguration.getKeystoreType().getType()); + Mockito.when(sslProperties.getTrustStore()).thenReturn(tlsConfiguration.getTruststorePath()); + Mockito.when(sslProperties.getTrustStorePassword()).thenReturn(tlsConfiguration.getTruststorePassword()); + Mockito.when(sslProperties.getTrustStoreType()).thenReturn(tlsConfiguration.getTruststoreType().getType()); + Mockito.when(sslProperties.getEnabledProtocols()).thenReturn(Arrays.stream(tlsConfiguration.getEnabledProtocols()) + .collect(Collectors.joining(","))); + Mockito.when(sslProperties.getEnabledCipherSuites()).thenReturn(CIPHER_SUITE_VALUE); + + Mockito.when(properties.getUri()).thenReturn(URI_VALUE.replace("http", "https")); + this.configureService(); + + this.ensureTlsPropertiesAccessed(1); + } finally { + Files.deleteIfExists(Paths.get(tlsConfiguration.getKeystorePath())); + Files.deleteIfExists(Paths.get(tlsConfiguration.getTruststorePath())); + } + } +} diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml index 7bda15b889..5ef5bb49c4 100644 --- a/nifi-commons/pom.xml +++ b/nifi-commons/pom.xml @@ -48,6 +48,7 @@ nifi-socket-utils nifi-utils nifi-uuid5 + nifi-vault-utils nifi-web-utils nifi-write-ahead-log