You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/09/22 02:11:24 UTC
[25/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring
project structure to better isolate extensions
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
new file mode 100644
index 0000000..1dcb0f7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -0,0 +1,305 @@
+/*
+ * 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.registry.properties;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NiFiRegistryProperties extends Properties {
+
+ private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryProperties.class);
+
+ // Keys
+ public static final String WEB_WAR_DIR = "nifi.registry.web.war.directory";
+ public static final String WEB_HTTP_PORT = "nifi.registry.web.http.port";
+ public static final String WEB_HTTP_HOST = "nifi.registry.web.http.host";
+ public static final String WEB_HTTPS_PORT = "nifi.registry.web.https.port";
+ public static final String WEB_HTTPS_HOST = "nifi.registry.web.https.host";
+ public static final String WEB_WORKING_DIR = "nifi.registry.web.jetty.working.directory";
+ public static final String WEB_THREADS = "nifi.registry.web.jetty.threads";
+
+ public static final String SECURITY_KEYSTORE = "nifi.registry.security.keystore";
+ public static final String SECURITY_KEYSTORE_TYPE = "nifi.registry.security.keystoreType";
+ public static final String SECURITY_KEYSTORE_PASSWD = "nifi.registry.security.keystorePasswd";
+ public static final String SECURITY_KEY_PASSWD = "nifi.registry.security.keyPasswd";
+ public static final String SECURITY_TRUSTSTORE = "nifi.registry.security.truststore";
+ public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.registry.security.truststoreType";
+ public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.registry.security.truststorePasswd";
+ public static final String SECURITY_NEED_CLIENT_AUTH = "nifi.registry.security.needClientAuth";
+ public static final String SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "nifi.registry.security.authorizers.configuration.file";
+ public static final String SECURITY_AUTHORIZER = "nifi.registry.security.authorizer";
+ public static final String SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE = "nifi.registry.security.identity.providers.configuration.file";
+ public static final String SECURITY_IDENTITY_PROVIDER = "nifi.registry.security.identity.provider";
+ public static final String SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX = "nifi.registry.security.identity.mapping.pattern.";
+ public static final String SECURITY_IDENTITY_MAPPING_VALUE_PREFIX = "nifi.registry.security.identity.mapping.value.";
+
+ public static final String EXTENSION_DIR_PREFIX = "nifi.registry.extension.dir.";
+
+ public static final String PROVIDERS_CONFIGURATION_FILE = "nifi.registry.providers.configuration.file";
+
+ // Original DB properties
+ public static final String DATABASE_DIRECTORY = "nifi.registry.db.directory";
+ public static final String DATABASE_URL_APPEND = "nifi.registry.db.url.append";
+
+ // New style DB properties
+ public static final String DATABASE_URL = "nifi.registry.db.url";
+ public static final String DATABASE_DRIVER_CLASS_NAME = "nifi.registry.db.driver.class";
+ public static final String DATABASE_DRIVER_DIR = "nifi.registry.db.driver.directory";
+ public static final String DATABASE_USERNAME = "nifi.registry.db.username";
+ public static final String DATABASE_PASSWORD = "nifi.registry.db.password";
+ public static final String DATABASE_MAX_CONNECTIONS = "nifi.registry.db.maxConnections";
+ public static final String DATABASE_SQL_DEBUG = "nifi.registry.db.sql.debug";
+
+ // Kerberos properties
+ public static final String KERBEROS_KRB5_FILE = "nifi.registry.kerberos.krb5.file";
+ public static final String KERBEROS_SPNEGO_PRINCIPAL = "nifi.registry.kerberos.spnego.principal";
+ public static final String KERBEROS_SPNEGO_KEYTAB_LOCATION = "nifi.registry.kerberos.spnego.keytab.location";
+ public static final String KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION = "nifi.registry.kerberos.spnego.authentication.expiration";
+ public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.registry.kerberos.service.principal";
+ public static final String KERBEROS_SERVICE_KEYTAB_LOCATION = "nifi.registry.kerberos.service.keytab.location";
+
+ // Defaults
+ public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty";
+ public static final String DEFAULT_WAR_DIR = "./lib";
+ public static final String DEFAULT_PROVIDERS_CONFIGURATION_FILE = "./conf/providers.xml";
+ public static final String DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "./conf/authorizers.xml";
+ public static final String DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE = "./conf/identity-providers.xml";
+ public static final String DEFAULT_AUTHENTICATION_EXPIRATION = "12 hours";
+
+ public int getWebThreads() {
+ int webThreads = 200;
+ try {
+ webThreads = Integer.parseInt(getProperty(WEB_THREADS));
+ } catch (final NumberFormatException nfe) {
+ logger.warn(String.format("%s must be an integer value. Defaulting to %s", WEB_THREADS, webThreads));
+ }
+ return webThreads;
+ }
+
+ public Integer getPort() {
+ return getPropertyAsInteger(WEB_HTTP_PORT);
+ }
+
+ public String getHttpHost() {
+ return getProperty(WEB_HTTP_HOST);
+ }
+
+ public Integer getSslPort() {
+ return getPropertyAsInteger(WEB_HTTPS_PORT);
+ }
+
+ public String getHttpsHost() {
+ return getProperty(WEB_HTTPS_HOST);
+ }
+
+ public boolean getNeedClientAuth() {
+ boolean needClientAuth = true;
+ String rawNeedClientAuth = getProperty(SECURITY_NEED_CLIENT_AUTH);
+ if ("false".equalsIgnoreCase(rawNeedClientAuth)) {
+ needClientAuth = false;
+ }
+ return needClientAuth;
+ }
+
+ public String getKeyStorePath() {
+ return getProperty(SECURITY_KEYSTORE);
+ }
+
+ public String getKeyStoreType() {
+ return getProperty(SECURITY_KEYSTORE_TYPE);
+ }
+
+ public String getKeyStorePassword() {
+ return getProperty(SECURITY_KEYSTORE_PASSWD);
+ }
+
+ public String getKeyPassword() {
+ return getProperty(SECURITY_KEY_PASSWD);
+ }
+
+ public String getTrustStorePath() {
+ return getProperty(SECURITY_TRUSTSTORE);
+ }
+
+ public String getTrustStoreType() {
+ return getProperty(SECURITY_TRUSTSTORE_TYPE);
+ }
+
+ public String getTrustStorePassword() {
+ return getProperty(SECURITY_TRUSTSTORE_PASSWD);
+ }
+
+ public File getWarLibDirectory() {
+ return new File(getProperty(WEB_WAR_DIR, DEFAULT_WAR_DIR));
+ }
+
+ public File getWebWorkingDirectory() {
+ return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR));
+ }
+
+ public File getProvidersConfigurationFile() {
+ return getPropertyAsFile(PROVIDERS_CONFIGURATION_FILE, DEFAULT_PROVIDERS_CONFIGURATION_FILE);
+ }
+
+ public String getLegacyDatabaseDirectory() {
+ return getProperty(DATABASE_DIRECTORY);
+ }
+
+ public String getLegacyDatabaseUrlAppend() {
+ return getProperty(DATABASE_URL_APPEND);
+ }
+
+ public String getDatabaseUrl() {
+ return getProperty(DATABASE_URL);
+ }
+
+ public String getDatabaseDriverClassName() {
+ return getProperty(DATABASE_DRIVER_CLASS_NAME);
+ }
+
+ public String getDatabaseDriverDirectory() {
+ return getProperty(DATABASE_DRIVER_DIR);
+ }
+
+ public String getDatabaseUsername() {
+ return getProperty(DATABASE_USERNAME);
+ }
+
+ public String getDatabasePassword() {
+ return getProperty(DATABASE_PASSWORD);
+ }
+
+ public Integer getDatabaseMaxConnections() {
+ return getPropertyAsInteger(DATABASE_MAX_CONNECTIONS);
+ }
+
+ public boolean getDatabaseSqlDebug() {
+ final String value = getProperty(DATABASE_SQL_DEBUG);
+
+ if (StringUtils.isBlank(value)) {
+ return false;
+ }
+
+ return "true".equalsIgnoreCase(value.trim());
+ }
+
+ public File getAuthorizersConfigurationFile() {
+ return getPropertyAsFile(SECURITY_AUTHORIZERS_CONFIGURATION_FILE, DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE);
+ }
+
+ public File getIdentityProviderConfigurationFile() {
+ return getPropertyAsFile(SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE, DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE);
+ }
+
+ public File getKerberosConfigurationFile() {
+ return getPropertyAsFile(KERBEROS_KRB5_FILE);
+ }
+
+ public String getKerberosSpnegoAuthenticationExpiration() {
+ return getProperty(KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION, DEFAULT_AUTHENTICATION_EXPIRATION);
+ }
+
+ public String getKerberosSpnegoPrincipal() {
+ return getPropertyAsTrimmedString(KERBEROS_SPNEGO_PRINCIPAL);
+ }
+
+ public String getKerberosSpnegoKeytabLocation() {
+ return getPropertyAsTrimmedString(KERBEROS_SPNEGO_KEYTAB_LOCATION);
+ }
+
+ public boolean isKerberosSpnegoSupportEnabled() {
+ return !StringUtils.isBlank(getKerberosSpnegoPrincipal()) && !StringUtils.isBlank(getKerberosSpnegoKeytabLocation());
+ }
+
+ public String getKerberosServicePrincipal() {
+ return getPropertyAsTrimmedString(KERBEROS_SERVICE_PRINCIPAL);
+ }
+
+ public String getKerberosServiceKeytabLocation() {
+ return getPropertyAsTrimmedString(KERBEROS_SERVICE_KEYTAB_LOCATION);
+ }
+
+ public Set<String> getExtensionsDirs() {
+ final Set<String> extensionDirs = new HashSet<>();
+ stringPropertyNames().stream().filter(key -> key.startsWith(EXTENSION_DIR_PREFIX)).forEach(key -> extensionDirs.add(getProperty(key)));
+ return extensionDirs;
+ }
+
+ /**
+ * Retrieves all known property keys.
+ *
+ * @return all known property keys
+ */
+ public Set<String> getPropertyKeys() {
+ Set<String> propertyNames = new HashSet<>();
+ Enumeration e = this.propertyNames();
+ for (; e.hasMoreElements(); ){
+ propertyNames.add((String) e.nextElement());
+ }
+
+ return propertyNames;
+ }
+
+ // Helper functions for common ways of interpreting property values
+
+ private String getPropertyAsTrimmedString(String key) {
+ final String value = getProperty(key);
+ if (!StringUtils.isBlank(value)) {
+ return value.trim();
+ } else {
+ return null;
+ }
+ }
+
+ private Integer getPropertyAsInteger(String key) {
+ final String value = getProperty(key);
+ if (StringUtils.isBlank(value)) {
+ return null;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (final NumberFormatException nfe) {
+ throw new IllegalStateException(String.format("%s must be an integer value.", key));
+ }
+ }
+
+ private File getPropertyAsFile(String key) {
+ final String filePath = getProperty(key);
+ if (filePath != null && filePath.trim().length() > 0) {
+ return new File(filePath.trim());
+ } else {
+ return null;
+ }
+ }
+
+ private File getPropertyAsFile(String propertyKey, String defaultFileLocation) {
+ final String value = getProperty(propertyKey);
+ if (StringUtils.isBlank(value)) {
+ return new File(defaultFileLocation);
+ } else {
+ return new File(value);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
new file mode 100644
index 0000000..5ceffd1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
@@ -0,0 +1,148 @@
+/*
+ * 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.registry.properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+public class NiFiRegistryPropertiesLoader {
+
+ private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesLoader.class);
+
+ private static final String RELATIVE_PATH = "conf/nifi-registry.properties";
+
+ private String keyHex;
+
+ // Future enhancement: allow for external registration of new providers
+ private static SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
+
+ /**
+ * Returns an instance of the loader configured with the key.
+ * <p>
+ * <p>
+ * NOTE: This method is used reflectively by the process which starts NiFi
+ * so changes to it must be made in conjunction with that mechanism.</p>
+ *
+ * @param keyHex the key used to encrypt any sensitive properties
+ * @return the configured loader
+ */
+ public static NiFiRegistryPropertiesLoader withKey(String keyHex) {
+ NiFiRegistryPropertiesLoader loader = new NiFiRegistryPropertiesLoader();
+ loader.setKeyHex(keyHex);
+ return loader;
+ }
+
+ /**
+ * Sets the hexadecimal key used to unprotect properties encrypted with
+ * {@link AESSensitivePropertyProvider}. If the key has already been set,
+ * calling this method will throw a {@link RuntimeException}.
+ *
+ * @param keyHex the key in hexadecimal format
+ */
+ public void setKeyHex(String keyHex) {
+ if (this.keyHex == null || this.keyHex.trim().isEmpty()) {
+ this.keyHex = keyHex;
+ } else {
+ throw new RuntimeException("Cannot overwrite an existing key");
+ }
+ }
+
+ private static String getDefaultProviderKey() {
+ try {
+ return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? "256" : "128");
+ } catch (NoSuchAlgorithmException e) {
+ return "aes/gcm/128";
+ }
+ }
+
+ private void initializeSensitivePropertyProviderFactory() {
+ sensitivePropertyProviderFactory = new AESSensitivePropertyProviderFactory(keyHex);
+ }
+
+ private SensitivePropertyProvider getSensitivePropertyProvider() {
+ initializeSensitivePropertyProviderFactory();
+ return sensitivePropertyProviderFactory.getProvider();
+ }
+
+ /**
+ * Returns a {@link ProtectedNiFiRegistryProperties} instance loaded from the
+ * serialized form in the file. Responsible for actually reading from disk
+ * and deserializing the properties. Returns a protected instance to allow
+ * for decryption operations.
+ *
+ * @param file the file containing serialized properties
+ * @return the ProtectedNiFiProperties instance
+ */
+ ProtectedNiFiRegistryProperties readProtectedPropertiesFromDisk(File file) {
+ if (file == null || !file.exists() || !file.canRead()) {
+ String path = (file == null ? "missing file" : file.getAbsolutePath());
+ logger.error("Cannot read from '{}' -- file is missing or not readable", path);
+ throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable");
+ }
+
+ final NiFiRegistryProperties rawProperties = new NiFiRegistryProperties();
+ try (final FileReader reader = new FileReader(file)) {
+ rawProperties.load(reader);
+ logger.info("Loaded {} properties from {}", rawProperties.size(), file.getAbsolutePath());
+ ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = new ProtectedNiFiRegistryProperties(rawProperties);
+ return protectedNiFiRegistryProperties;
+ } catch (final IOException ioe) {
+ logger.error("Cannot load properties file due to " + ioe.getLocalizedMessage());
+ throw new RuntimeException("Cannot load properties file due to " + ioe.getLocalizedMessage(), ioe);
+ }
+ }
+
+ /**
+ * Returns an instance of {@link NiFiRegistryProperties} loaded from the provided
+ * {@link File}. If any properties are protected, will attempt to use the appropriate
+ * {@link SensitivePropertyProvider} to unprotect them transparently.
+ *
+ * @param file the File containing the serialized properties
+ * @return the NiFiProperties instance
+ */
+ public NiFiRegistryProperties load(File file) {
+ ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = readProtectedPropertiesFromDisk(file);
+ if (protectedNiFiRegistryProperties.hasProtectedKeys()) {
+ protectedNiFiRegistryProperties.addSensitivePropertyProvider(getSensitivePropertyProvider());
+ }
+
+ return protectedNiFiRegistryProperties.getUnprotectedProperties();
+ }
+
+ /**
+ * Returns an instance of {@link NiFiRegistryProperties}. The path must not be empty.
+ *
+ * @param path the path of the serialized properties file
+ * @return the NiFiRegistryProperties instance
+ * @see NiFiRegistryPropertiesLoader#load(File)
+ */
+ public NiFiRegistryProperties load(String path) {
+ if (path != null && !path.trim().isEmpty()) {
+ return load(new File(path));
+ } else {
+ logger.error("Cannot read from '{}' -- path is null or empty", path);
+ throw new IllegalArgumentException("NiFi Registry properties file path empty or null");
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
new file mode 100644
index 0000000..5debc4a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
@@ -0,0 +1,528 @@
+/*
+ * 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.registry.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Wrapper class of {@link NiFiRegistryProperties} for intermediate phase when
+ * {@link NiFiRegistryPropertiesLoader} loads the raw properties file and performs
+ * unprotection activities before returning an instance of {@link NiFiRegistryProperties}.
+ */
+class ProtectedNiFiRegistryProperties {
+ private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiRegistryProperties.class);
+
+ private NiFiRegistryProperties properties;
+
+ private Map<String, SensitivePropertyProvider> localProviderCache = new HashMap<>();
+
+ // Additional "sensitive" property key
+ public static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = "nifi.registry.sensitive.props.additional.keys";
+
+ // Default list of "sensitive" property keys
+ public static final List<String> DEFAULT_SENSITIVE_PROPERTIES = new ArrayList<>(asList(
+ NiFiRegistryProperties.SECURITY_KEY_PASSWD,
+ NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD,
+ NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD));
+
+ public ProtectedNiFiRegistryProperties() {
+ this(null);
+ }
+
+ /**
+ * Creates an instance containing the provided {@link NiFiRegistryProperties}.
+ *
+ * @param props the NiFiProperties to contain
+ */
+ public ProtectedNiFiRegistryProperties(NiFiRegistryProperties props) {
+ if (props == null) {
+ props = new NiFiRegistryProperties();
+ }
+ this.properties = props;
+ logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiProperties",
+ getPropertyKeysIncludingProtectionSchemes().size(), getProtectedPropertyKeys().size());
+ }
+
+ /**
+ * Retrieves the property value for the given property key.
+ *
+ * @param key the key of property value to lookup
+ * @return value of property at given key or null if not found
+ */
+ // @Override
+ public String getProperty(String key) {
+ return getInternalNiFiProperties().getProperty(key);
+ }
+
+ /**
+ * Returns the internal representation of the {@link NiFiRegistryProperties} -- protected
+ * or not as determined by the current state. No guarantee is made to the
+ * protection state of these properties. If the internal reference is null, a new
+ * {@link NiFiRegistryProperties} instance is created.
+ *
+ * @return the internal properties
+ */
+ NiFiRegistryProperties getInternalNiFiProperties() {
+ if (this.properties == null) {
+ this.properties = new NiFiRegistryProperties();
+ }
+
+ return this.properties;
+ }
+
+ /**
+ * Returns the number of properties in the NiFiRegistryProperties,
+ * excluding protection scheme properties.
+ *
+ * <p>
+ * Example:
+ * <p>
+ * key: E(value, key)
+ * key.protected: aes/gcm/256
+ * key2: value2
+ * <p>
+ * would return size 2
+ *
+ * @return the count of real properties
+ */
+ int size() {
+ return getPropertyKeysExcludingProtectionSchemes().size();
+ }
+
+ /**
+ * Returns the complete set of property keys in the NiFiRegistryProperties,
+ * including any protection keys (i.e. 'x.y.z.protected').
+ *
+ * @return the set of property keys
+ */
+ Set<String> getPropertyKeysIncludingProtectionSchemes() {
+ return getInternalNiFiProperties().getPropertyKeys();
+ }
+
+ /**
+ * Returns the set of property keys in the NiFiRegistryProperties,
+ * excluding any protection keys (i.e. 'x.y.z.protected').
+ *
+ * @return the set of property keys
+ */
+ Set<String> getPropertyKeysExcludingProtectionSchemes() {
+ Set<String> filteredKeys = getPropertyKeysIncludingProtectionSchemes();
+ filteredKeys.removeIf(p -> p.endsWith(".protected"));
+ return filteredKeys;
+ }
+
+ /**
+ * Splits a single string containing multiple property keys into a List.
+ *
+ * Delimited by ',' or ';' and ignores leading and trailing whitespace around delimiter.
+ *
+ * @param multipleProperties a single String containing multiple properties, i.e.
+ * "nifi.registry.property.1; nifi.registry.property.2, nifi.registry.property.3"
+ * @return a List containing the split and trimmed properties
+ */
+ private static List<String> splitMultipleProperties(String multipleProperties) {
+ if (multipleProperties == null || multipleProperties.trim().isEmpty()) {
+ return new ArrayList<>(0);
+ } else {
+ List<String> properties = new ArrayList<>(asList(multipleProperties.split("\\s*[,;]\\s*")));
+ for (int i = 0; i < properties.size(); i++) {
+ properties.set(i, properties.get(i).trim());
+ }
+ return properties;
+ }
+ }
+
+ /**
+ * Returns a list of the keys identifying "sensitive" properties.
+ *
+ * There is a default list, and additional keys can be provided in the
+ * {@code nifi.registry.sensitive.props.additional.keys} property in {@code nifi-registry.properties}.
+ *
+ * @return the list of sensitive property keys
+ */
+ public List<String> getSensitivePropertyKeys() {
+ String additionalPropertiesString = getProperty(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+ if (additionalPropertiesString == null || additionalPropertiesString.trim().isEmpty()) {
+ return DEFAULT_SENSITIVE_PROPERTIES;
+ } else {
+ List<String> additionalProperties = splitMultipleProperties(additionalPropertiesString);
+ /* Remove this key if it was accidentally provided as a sensitive key
+ * because we cannot protect it and read from it
+ */
+ if (additionalProperties.contains(ADDITIONAL_SENSITIVE_PROPERTIES_KEY)) {
+ logger.warn("The key '{}' contains itself. This is poor practice and should be removed", ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+ additionalProperties.remove(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+ }
+ additionalProperties.addAll(DEFAULT_SENSITIVE_PROPERTIES);
+ return additionalProperties;
+ }
+ }
+
+ /**
+ * Returns a list of the keys identifying "sensitive" properties. There is a default list,
+ * and additional keys can be provided in the {@code nifi.sensitive.props.additional.keys} property in {@code nifi.properties}.
+ *
+ * @return the list of sensitive property keys
+ */
+ public List<String> getPopulatedSensitivePropertyKeys() {
+ List<String> allSensitiveKeys = getSensitivePropertyKeys();
+ return allSensitiveKeys.stream().filter(k -> StringUtils.isNotBlank(getProperty(k))).collect(Collectors.toList());
+ }
+
+ /**
+ * Returns true if any sensitive keys are protected.
+ *
+ * @return true if any key is protected; false otherwise
+ */
+ public boolean hasProtectedKeys() {
+ List<String> sensitiveKeys = getSensitivePropertyKeys();
+ for (String k : sensitiveKeys) {
+ if (isPropertyProtected(k)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a Map of the keys identifying "sensitive" properties that are currently protected and the "protection" key for each.
+ *
+ * This may or may not include all properties marked as sensitive.
+ *
+ * @return the Map of protected property keys and the protection identifier for each
+ */
+ public Map<String, String> getProtectedPropertyKeys() {
+ List<String> sensitiveKeys = getSensitivePropertyKeys();
+
+ Map<String, String> traditionalProtectedProperties = new HashMap<>();
+ for (String key : sensitiveKeys) {
+ String protection = getProperty(getProtectionKey(key));
+ if (StringUtils.isNotBlank(protection) && StringUtils.isNotBlank(getProperty(key))) {
+ traditionalProtectedProperties.put(key, protection);
+ }
+ }
+
+ return traditionalProtectedProperties;
+ }
+
+ /**
+ * Returns the unique set of all protection schemes currently in use for this instance.
+ *
+ * @return the set of protection schemes
+ */
+ public Set<String> getProtectionSchemes() {
+ return new HashSet<>(getProtectedPropertyKeys().values());
+ }
+
+ /**
+ * Returns a percentage of the total number of populated properties marked as sensitive that are currently protected.
+ *
+ * @return the percent of sensitive properties marked as protected
+ */
+ public int getPercentOfSensitivePropertiesProtected() {
+ return (int) Math.round(getProtectedPropertyKeys().size() / ((double) getPopulatedSensitivePropertyKeys().size()) * 100);
+ }
+
+ /**
+ * Returns true if the property identified by this key is considered sensitive in this instance of {@code NiFiProperties}.
+ * Some properties are sensitive by default, while others can be specified by
+ * {@link ProtectedNiFiRegistryProperties#ADDITIONAL_SENSITIVE_PROPERTIES_KEY}.
+ *
+ * @param key the key
+ * @return true if it is sensitive
+ * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys()
+ */
+ public boolean isPropertySensitive(String key) {
+ // If the explicit check for ADDITIONAL_SENSITIVE_PROPERTIES_KEY is not here, this could loop infinitely
+ return key != null && !key.equals(ADDITIONAL_SENSITIVE_PROPERTIES_KEY) && getSensitivePropertyKeys().contains(key.trim());
+ }
+
+ /**
+ * Returns true if the property identified by this key is considered protected in this instance of {@code NiFiProperties}.
+ * The property value is protected if the key is sensitive and the sibling key of key.protected is present.
+ *
+ * @param key the key
+ * @return true if it is currently marked as protected
+ * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys()
+ */
+ public boolean isPropertyProtected(String key) {
+ return key != null && isPropertySensitive(key) && !StringUtils.isBlank(getProperty(getProtectionKey(key)));
+ }
+
+ /**
+ * Returns the sibling property key which specifies the protection scheme for this key.
+ * <p>
+ * Example:
+ * <p>
+ * nifi.registry.sensitive.key=ABCXYZ
+ * nifi.registry.sensitive.key.protected=aes/gcm/256
+ * <p>
+ * nifi.registry.sensitive.key -> nifi.sensitive.key.protected
+ *
+ * @param key the key identifying the sensitive property
+ * @return the key identifying the protection scheme for the sensitive property
+ */
+ public static String getProtectionKey(String key) {
+ if (key == null || key.isEmpty()) {
+ throw new IllegalArgumentException("Cannot find protection key for null key");
+ }
+
+ return key + ".protected";
+ }
+
+ /**
+ * Returns the unprotected {@link NiFiRegistryProperties} instance. If none of the
+ * properties loaded are marked as protected, it will simply pass through the
+ * internal instance. If any are protected, it will drop the protection scheme keys
+ * and translate each protected value (encrypted, HSM-retrieved, etc.) into the raw
+ * value and store it under the original key.
+ * <p>
+ * If any property fails to unprotect, it will save that key and continue. After
+ * attempting all properties, it will throw an exception containing all failed
+ * properties. This is necessary because the order is not enforced, so all failed
+ * properties should be gathered together.
+ *
+ * @return the NiFiRegistryProperties instance with all raw values
+ * @throws SensitivePropertyProtectionException if there is a problem unprotecting one or more keys
+ */
+ public NiFiRegistryProperties getUnprotectedProperties() throws SensitivePropertyProtectionException {
+ if (hasProtectedKeys()) {
+ logger.debug("There are {} protected properties of {} sensitive properties ({}%)",
+ getProtectedPropertyKeys().size(),
+ getPopulatedSensitivePropertyKeys().size(),
+ getPercentOfSensitivePropertiesProtected());
+
+ NiFiRegistryProperties unprotectedProperties = new NiFiRegistryProperties();
+
+ Set<String> failedKeys = new HashSet<>();
+
+ for (String key : getPropertyKeysExcludingProtectionSchemes()) {
+ /* Three kinds of keys
+ * 1. protection schemes -- skip
+ * 2. protected keys -- unprotect and copy
+ * 3. normal keys -- copy over
+ */
+ if (key.endsWith(".protected")) {
+ // Do nothing
+ } else if (isPropertyProtected(key)) {
+ try {
+ unprotectedProperties.setProperty(key, unprotectValue(key, getProperty(key)));
+ } catch (SensitivePropertyProtectionException e) {
+ logger.warn("Failed to unprotect '{}'", key, e);
+ failedKeys.add(key);
+ }
+ } else {
+ unprotectedProperties.setProperty(key, getProperty(key));
+ }
+ }
+
+ if (!failedKeys.isEmpty()) {
+ if (failedKeys.size() > 1) {
+ logger.warn("Combining {} failed keys [{}] into single exception", failedKeys.size(), StringUtils.join(failedKeys, ", "));
+ throw new MultipleSensitivePropertyProtectionException("Failed to unprotect keys", failedKeys);
+ } else {
+ throw new SensitivePropertyProtectionException("Failed to unprotect key " + failedKeys.iterator().next());
+ }
+ }
+
+ return unprotectedProperties;
+ } else {
+ logger.debug("No protected properties");
+ return getInternalNiFiProperties();
+ }
+ }
+
+ /**
+ * Registers a new {@link SensitivePropertyProvider}. This method will throw a {@link UnsupportedOperationException} if a provider is already registered for the protection scheme.
+ *
+ * @param sensitivePropertyProvider the provider
+ */
+ void addSensitivePropertyProvider(SensitivePropertyProvider sensitivePropertyProvider) {
+ if (sensitivePropertyProvider == null) {
+ throw new IllegalArgumentException("Cannot add null SensitivePropertyProvider");
+ }
+
+ if (getSensitivePropertyProviders().containsKey(sensitivePropertyProvider.getIdentifierKey())) {
+ throw new UnsupportedOperationException("Cannot overwrite existing sensitive property provider registered for " + sensitivePropertyProvider.getIdentifierKey());
+ }
+
+ getSensitivePropertyProviders().put(sensitivePropertyProvider.getIdentifierKey(), sensitivePropertyProvider);
+ }
+
+ private String getDefaultProtectionScheme() {
+ if (!getSensitivePropertyProviders().isEmpty()) {
+ List<String> schemes = new ArrayList<>(getSensitivePropertyProviders().keySet());
+ Collections.sort(schemes);
+ return schemes.get(0);
+ } else {
+ throw new IllegalStateException("No registered protection schemes");
+ }
+ }
+
+ /**
+ * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the default protection scheme.
+ *
+ * Plain non-sensitive values are copied directly.
+ *
+ * @return the protected properties in a {@link NiFiRegistryProperties} object
+ * @throws IllegalStateException if no protection schemes are registered
+ */
+ NiFiRegistryProperties protectPlainProperties() {
+ try {
+ return protectPlainProperties(getDefaultProtectionScheme());
+ } catch (IllegalStateException e) {
+ final String msg = "Cannot protect properties with default scheme if no protection schemes are registered";
+ logger.warn(msg);
+ throw new IllegalStateException(msg, e);
+ }
+ }
+
+ /**
+ * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the provided protection scheme.
+ *
+ * Plain non-sensitive values are copied directly.
+ *
+ * @param protectionScheme the identifier key of the {@link SensitivePropertyProvider} to use
+ * @return the protected properties in a {@link NiFiRegistryProperties} object
+ */
+ NiFiRegistryProperties protectPlainProperties(String protectionScheme) {
+ SensitivePropertyProvider spp = getSensitivePropertyProvider(protectionScheme);
+
+ NiFiRegistryProperties protectedProperties = new NiFiRegistryProperties();
+
+ // Copy over the plain keys
+ Set<String> plainKeys = getPropertyKeysExcludingProtectionSchemes();
+ plainKeys.removeAll(getSensitivePropertyKeys());
+ for (String key : plainKeys) {
+ protectedProperties.setProperty(key, getInternalNiFiProperties().getProperty(key));
+ }
+
+ // Add the protected keys and the protection schemes
+ for (String key : getSensitivePropertyKeys()) {
+ final String plainValue = getProperty(key);
+ if (plainValue != null && !plainValue.trim().isEmpty()) {
+ final String protectedValue = spp.protect(plainValue);
+ protectedProperties.setProperty(key, protectedValue);
+ protectedProperties.setProperty(getProtectionKey(key), protectionScheme);
+ }
+ }
+
+ return protectedProperties;
+ }
+
+ /**
+ * Returns the number of properties that are marked as protected in the provided {@link NiFiRegistryProperties} instance
+ * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance.
+ *
+ * @param plainProperties the instance to count protected properties
+ * @return the number of protected properties
+ */
+ public static int countProtectedProperties(NiFiRegistryProperties plainProperties) {
+ return new ProtectedNiFiRegistryProperties(plainProperties).getProtectedPropertyKeys().size();
+ }
+
+ /**
+ * Returns the number of properties that are marked as sensitive in the provided {@link NiFiRegistryProperties} instance
+ * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance.
+ *
+ * @param plainProperties the instance to count sensitive properties
+ * @return the number of sensitive properties
+ */
+ public static int countSensitiveProperties(NiFiRegistryProperties plainProperties) {
+ return new ProtectedNiFiRegistryProperties(plainProperties).getSensitivePropertyKeys().size();
+ }
+
+ @Override
+ public String toString() {
+ final Set<String> providers = getSensitivePropertyProviders().keySet();
+ return new StringBuilder("ProtectedNiFiProperties instance with ")
+ .append(getPropertyKeysIncludingProtectionSchemes().size())
+ .append(" properties (")
+ .append(getProtectedPropertyKeys().size())
+ .append(" protected) and ")
+ .append(providers.size())
+ .append(" sensitive property providers: ")
+ .append(StringUtils.join(providers, ", "))
+ .toString();
+ }
+
+ /**
+ * Returns the local provider cache (null-safe) as a Map of protection schemes -> implementations.
+ *
+ * @return the map
+ */
+ private Map<String, SensitivePropertyProvider> getSensitivePropertyProviders() {
+ if (localProviderCache == null) {
+ localProviderCache = new HashMap<>();
+ }
+
+ return localProviderCache;
+ }
+
+ private SensitivePropertyProvider getSensitivePropertyProvider(String protectionScheme) {
+ if (isProviderAvailable(protectionScheme)) {
+ return getSensitivePropertyProviders().get(protectionScheme);
+ } else {
+ throw new SensitivePropertyProtectionException("No provider available for " + protectionScheme);
+ }
+ }
+
+ private boolean isProviderAvailable(String protectionScheme) {
+ return getSensitivePropertyProviders().containsKey(protectionScheme);
+ }
+
+ /**
+ * If the value is protected, unprotects it and returns it. If not, returns the original value.
+ *
+ * @param key the retrieved property key
+ * @param retrievedValue the retrieved property value
+ * @return the unprotected value
+ */
+ private String unprotectValue(String key, String retrievedValue) {
+ // Checks if the key is sensitive and marked as protected
+ if (isPropertyProtected(key)) {
+ final String protectionScheme = getProperty(getProtectionKey(key));
+
+ // No provider registered for this scheme, so just return the value
+ if (!isProviderAvailable(protectionScheme)) {
+ logger.warn("No provider available for {} so passing the protected {} value back", protectionScheme, key);
+ return retrievedValue;
+ }
+
+ try {
+ SensitivePropertyProvider sensitivePropertyProvider = getSensitivePropertyProvider(protectionScheme);
+ return sensitivePropertyProvider.unprotect(retrievedValue);
+ } catch (SensitivePropertyProtectionException e) {
+ throw new SensitivePropertyProtectionException("Error unprotecting value for " + key, e.getCause());
+ }
+ }
+ return retrievedValue;
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
new file mode 100644
index 0000000..2ffa902
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
@@ -0,0 +1,89 @@
+/*
+ * 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.registry.properties;
+
+public class SensitivePropertyProtectionException extends RuntimeException {
+ /**
+ * Constructs a new throwable with {@code null} as its detail message.
+ * The cause is not initialized, and may subsequently be initialized by a
+ * call to {@link #initCause}.
+ * <p>
+ * <p>The {@link #fillInStackTrace()} method is called to initialize
+ * the stack trace data in the newly created throwable.
+ */
+ public SensitivePropertyProtectionException() {
+ }
+
+ /**
+ * Constructs a new throwable with the specified detail message. The
+ * cause is not initialized, and may subsequently be initialized by
+ * a call to {@link #initCause}.
+ * <p>
+ * <p>The {@link #fillInStackTrace()} method is called to initialize
+ * the stack trace data in the newly created throwable.
+ *
+ * @param message the detail message. The detail message is saved for
+ * later retrieval by the {@link #getMessage()} method.
+ */
+ public SensitivePropertyProtectionException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new throwable with the specified detail message and
+ * cause. <p>Note that the detail message associated with
+ * {@code cause} is <i>not</i> automatically incorporated in
+ * this throwable's detail message.
+ * <p>
+ * <p>The {@link #fillInStackTrace()} method is called to initialize
+ * the stack trace data in the newly created throwable.
+ *
+ * @param message the detail message (which is saved for later retrieval
+ * by the {@link #getMessage()} method).
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link #getCause()} method). (A {@code null} value is
+ * permitted, and indicates that the cause is nonexistent or
+ * unknown.)
+ */
+ public SensitivePropertyProtectionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new throwable with the specified cause and a detail
+ * message of {@code (cause==null ? null : cause.toString())} (which
+ * typically contains the class and detail message of {@code cause}).
+ * This constructor is useful for throwables that are little more than
+ * wrappers for other throwables (for example, PrivilegedActionException).
+ * <p>
+ * <p>The {@link #fillInStackTrace()} method is called to initialize
+ * the stack trace data in the newly created throwable.
+ *
+ * @param cause the cause (which is saved for later retrieval by the
+ * {@link #getCause()} method). (A {@code null} value is
+ * permitted, and indicates that the cause is nonexistent or
+ * unknown.)
+ */
+ public SensitivePropertyProtectionException(Throwable cause) {
+ super(cause);
+ }
+
+ @Override
+ public String toString() {
+ return "SensitivePropertyProtectionException: " + getLocalizedMessage();
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
new file mode 100644
index 0000000..c0dd43c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
@@ -0,0 +1,52 @@
+/*
+ * 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.registry.properties;
+
+public interface SensitivePropertyProvider {
+
+ /**
+ * Returns the name of the underlying implementation.
+ *
+ * @return the name of this sensitive property provider
+ */
+ String getName();
+
+ /**
+ * Returns the key used to identify the provider implementation in {@code nifi.properties}.
+ *
+ * @return the key to persist in the sibling property
+ */
+ String getIdentifierKey();
+
+ /**
+ * Returns the "protected" form of this value. This is a form which can safely be persisted in the {@code nifi.properties} file without compromising the value.
+ * An encryption-based provider would return a cipher text, while a remote-lookup provider could return a unique ID to retrieve the secured value.
+ *
+ * @param unprotectedValue the sensitive value
+ * @return the value to persist in the {@code nifi.properties} file
+ */
+ String protect(String unprotectedValue) throws SensitivePropertyProtectionException;
+
+ /**
+ * Returns the "unprotected" form of this value. This is the raw sensitive value which is used by the application logic.
+ * An encryption-based provider would decrypt a cipher text and return the plaintext, while a remote-lookup provider could retrieve the secured value.
+ *
+ * @param protectedValue the protected value read from the {@code nifi.properties} file
+ * @return the raw value to be used by the application
+ */
+ String unprotect(String protectedValue) throws SensitivePropertyProtectionException;
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..c9d4313
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.registry.properties;
+
+public interface SensitivePropertyProviderFactory {
+
+ SensitivePropertyProvider getProvider();
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
new file mode 100644
index 0000000..df3bbe6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
@@ -0,0 +1,48 @@
+/*
+ * 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.registry.properties.util;
+
+import java.util.regex.Pattern;
+
+/**
+ * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties.
+ */
+public class IdentityMapping {
+
+ private final String key;
+ private final Pattern pattern;
+ private final String replacementValue;
+
+ public IdentityMapping(String key, Pattern pattern, String replacementValue) {
+ this.key = key;
+ this.pattern = pattern;
+ this.replacementValue = replacementValue;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public Pattern getPattern() {
+ return pattern;
+ }
+
+ public String getReplacementValue() {
+ return replacementValue;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
new file mode 100644
index 0000000..3c9208c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
@@ -0,0 +1,145 @@
+/*
+ * 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.registry.properties.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IdentityMappingUtil {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(IdentityMappingUtil.class);
+ private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)");
+
+ /**
+ * Builds the identity mappings from NiFiRegistryProperties.
+ *
+ * @param properties the NiFiRegistryProperties instance
+ * @return a list of identity mappings
+ */
+ public static List<IdentityMapping> getIdentityMappings(final NiFiRegistryProperties properties) {
+ final List<IdentityMapping> mappings = new ArrayList<>();
+
+ // go through each property
+ for (String propertyName : properties.getPropertyKeys()) {
+ if (StringUtils.startsWith(propertyName, NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) {
+ final String key = StringUtils.substringAfter(propertyName, NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX);
+ final String identityPattern = properties.getProperty(propertyName);
+
+ if (StringUtils.isBlank(identityPattern)) {
+ LOGGER.warn("Identity Mapping property {} was found, but was empty", new Object[]{propertyName});
+ continue;
+ }
+
+ final String identityValueProperty = NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX + key;
+ final String identityValue = properties.getProperty(identityValueProperty);
+
+ if (StringUtils.isBlank(identityValue)) {
+ LOGGER.warn("Identity Mapping property {} was found, but corresponding value {} was not found",
+ new Object[]{propertyName, identityValueProperty});
+ continue;
+ }
+
+ final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue);
+ mappings.add(identityMapping);
+
+ LOGGER.debug("Found Identity Mapping with key = {}, pattern = {}, value = {}",
+ new Object[] {key, identityPattern, identityValue});
+ }
+ }
+
+ // sort the list by the key so users can control the ordering in nifi-registry.properties
+ Collections.sort(mappings, new Comparator<IdentityMapping>() {
+ @Override
+ public int compare(IdentityMapping m1, IdentityMapping m2) {
+ return m1.getKey().compareTo(m2.getKey());
+ }
+ });
+
+ return mappings;
+ }
+
+ /**
+ * Checks the given identity against each provided mapping and performs the mapping using the first one that matches.
+ * If none match then the identity is returned as is.
+ *
+ * @param identity the identity to map
+ * @param mappings the mappings
+ * @return the mapped identity, or the same identity if no mappings matched
+ */
+ public static String mapIdentity(final String identity, List<IdentityMapping> mappings) {
+ for (IdentityMapping mapping : mappings) {
+ Matcher m = mapping.getPattern().matcher(identity);
+ if (m.matches()) {
+ final String pattern = mapping.getPattern().pattern();
+ final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount());
+ return identity.replaceAll(pattern, replacementValue);
+ }
+ }
+
+ return identity;
+ }
+
+ // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing
+ // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather
+ // than attempting to use it as a back reference.
+ private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) {
+ if (numCapturingGroups == 0) {
+ return unescaped;
+ }
+
+ String value = unescaped;
+ final Matcher backRefMatcher = backReferencePattern.matcher(value);
+ while (backRefMatcher.find()) {
+ final String backRefNum = backRefMatcher.group(1);
+ if (backRefNum.startsWith("0")) {
+ continue;
+ }
+ final int originalBackRefIndex = Integer.parseInt(backRefNum);
+ int backRefIndex = originalBackRefIndex;
+
+ // if we have a replacement value like $123, and we have less than 123 capturing groups, then
+ // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups,
+ // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then
+ // we want to truncate the 1 and get 0.
+ while (backRefIndex > numCapturingGroups && backRefIndex >= 10) {
+ backRefIndex /= 10;
+ }
+
+ if (backRefIndex > numCapturingGroups) {
+ final StringBuilder sb = new StringBuilder(value.length() + 1);
+ final int groupStart = backRefMatcher.start(1);
+
+ sb.append(value.substring(0, groupStart - 1));
+ sb.append("\\");
+ sb.append(value.substring(groupStart - 1));
+ value = sb.toString();
+ }
+ }
+
+ return value;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
new file mode 100644
index 0000000..191b5e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.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.registry.security.crypto;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * An implementation of {@link CryptoKeyProvider} that loads the key from disk every time it is needed.
+ *
+ * The persistence-backing of the key is in the bootstrap.conf file, which must be provided to the
+ * constructor of this class.
+ *
+ * As key access for sensitive value decryption is only used a few times during server initialization,
+ * this implementation trades efficiency for security by only keeping the key in memory with an
+ * in-scope reference for a brief period of time (assuming callers do not maintain an in-scope reference).
+ *
+ * @see CryptoKeyProvider
+ */
+public class BootstrapFileCryptoKeyProvider implements CryptoKeyProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(BootstrapFileCryptoKeyProvider.class);
+
+ private final String bootstrapFile;
+
+ /**
+ * Construct a new instance backed by the contents of a bootstrap.conf file.
+ *
+ * @param bootstrapFilePath The path to the bootstrap.conf file for this instance of NiFi Registry.
+ * Must not be null.
+ */
+ public BootstrapFileCryptoKeyProvider(final String bootstrapFilePath) {
+ if (bootstrapFilePath == null) {
+ throw new IllegalArgumentException(BootstrapFileCryptoKeyProvider.class.getSimpleName() + " cannot be initialized with null bootstrap file path.");
+ }
+ this.bootstrapFile = bootstrapFilePath;
+ }
+
+ /**
+ * @return The bootstrap file path that backs this provider instance.
+ */
+ public String getBootstrapFile() {
+ return bootstrapFile;
+ }
+
+ @Override
+ public String getKey() throws MissingCryptoKeyException {
+ try {
+ return CryptoKeyLoader.extractKeyFromBootstrapFile(this.bootstrapFile);
+ } catch (IOException ioe) {
+ final String errMsg = "Loading the master crypto key from bootstrap file '" + bootstrapFile + "' failed due to IOException.";
+ logger.warn(errMsg);
+ throw new MissingCryptoKeyException(errMsg, ioe);
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return "BootstrapFileCryptoKeyProvider{" +
+ "bootstrapFile='" + bootstrapFile + '\'' +
+ '}';
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
new file mode 100644
index 0000000..d828773
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
@@ -0,0 +1,87 @@
+/*
+ * 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.registry.security.crypto;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class CryptoKeyLoader {
+
+ private static final Logger logger = LoggerFactory.getLogger(CryptoKeyLoader.class);
+
+ private static final String BOOTSTRAP_KEY_PREFIX = "nifi.registry.bootstrap.sensitive.key=";
+
+ /**
+ * Returns the key (if any) used to encrypt sensitive properties.
+ * The key extracted from the bootstrap.conf file at the specified location.
+ *
+ * @param bootstrapPath the path to the bootstrap file
+ * @return the key in hexadecimal format, or {@link CryptoKeyProvider#EMPTY_KEY} if the key is null or empty
+ * @throws IOException if the file is not readable
+ */
+ public static String extractKeyFromBootstrapFile(String bootstrapPath) throws IOException {
+ File bootstrapFile;
+ if (StringUtils.isBlank(bootstrapPath)) {
+ logger.error("Cannot read from bootstrap.conf file to extract encryption key; location not specified");
+ throw new IOException("Cannot read from bootstrap.conf without file location");
+ } else {
+ bootstrapFile = new File(bootstrapPath);
+ }
+
+ String keyValue;
+ if (bootstrapFile.exists() && bootstrapFile.canRead()) {
+ try (Stream<String> stream = Files.lines(Paths.get(bootstrapFile.getAbsolutePath()))) {
+ Optional<String> keyLine = stream.filter(l -> l.startsWith(BOOTSTRAP_KEY_PREFIX)).findFirst();
+ if (keyLine.isPresent()) {
+ keyValue = keyLine.get().split("=", 2)[1];
+ keyValue = checkHexKey(keyValue);
+ } else {
+ keyValue = CryptoKeyProvider.EMPTY_KEY;
+ }
+ } catch (IOException e) {
+ logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key", bootstrapFile.getAbsolutePath());
+ throw new IOException("Cannot read from bootstrap.conf", e);
+ }
+ } else {
+ logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key -- file is missing or permissions are incorrect", bootstrapFile.getAbsolutePath());
+ throw new IOException("Cannot read from bootstrap.conf");
+ }
+
+ if (CryptoKeyProvider.EMPTY_KEY.equals(keyValue)) {
+ logger.info("No encryption key present in the bootstrap.conf file at {}", bootstrapFile.getAbsolutePath());
+ }
+
+ return keyValue;
+ }
+
+ private static String checkHexKey(String input) {
+ if (input == null || input.trim().isEmpty()) {
+ logger.debug("Checking the hex key value that was loaded determined the key is empty.");
+ return CryptoKeyProvider.EMPTY_KEY;
+ }
+ return input;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
new file mode 100644
index 0000000..bab8d7c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
@@ -0,0 +1,68 @@
+/*
+ * 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.registry.security.crypto;
+
+/**
+ * A simple interface that wraps a key that can be used for encryption and decryption.
+ * This allows for more flexibility with the lifecycle of keys and how other classes
+ * can declare dependencies for keys, by depending on a CryptoKeyProvider that will provided
+ * at runtime.
+ */
+public interface CryptoKeyProvider {
+
+ /**
+ * A string literal that indicates the contents of a key are empty.
+ * Can also be used in contexts that a null key is undesirable.
+ */
+ String EMPTY_KEY = "";
+
+ /**
+ * @return The crypto key known to this CryptoKeyProvider instance in hexadecimal format, or
+ * {@link #EMPTY_KEY} if the key is empty.
+ * @throws MissingCryptoKeyException if the key cannot be provided or determined for any reason.
+ * If the key is known to be empty, {@link #EMPTY_KEY} will be returned and a
+ * CryptoKeyMissingException will not be thrown
+ */
+ String getKey() throws MissingCryptoKeyException;
+
+ /**
+ * @return A boolean indicating if the key value held by this CryptoKeyProvider is empty,
+ * such as 'null' or empty string.
+ */
+ default boolean isEmpty() {
+ String key;
+ try {
+ key = getKey();
+ } catch (MissingCryptoKeyException e) {
+ return true;
+ }
+ return EMPTY_KEY.equals(key);
+ }
+
+ /**
+ * A string representation of this CryptoKeyProvider instance.
+ * <p>
+ * <p>
+ * Note: Implementations of this interface should take care not to leak sensitive
+ * key material in any strings they emmit, including in the toString implementation.
+ *
+ * @return A string representation of this CryptoKeyProvider instance.
+ */
+ @Override
+ public String toString();
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
new file mode 100644
index 0000000..dbc3752
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
@@ -0,0 +1,47 @@
+/*
+ * 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.registry.security.crypto;
+
+/**
+ * An exception type used by a {@link CryptoKeyProvider} when a request for the key
+ * cannot be fulfilled for any reason.
+ *
+ * @see CryptoKeyProvider
+ */
+public class MissingCryptoKeyException extends Exception {
+
+ public MissingCryptoKeyException() {
+ super();
+ }
+
+ public MissingCryptoKeyException(String message) {
+ super(message);
+ }
+
+ public MissingCryptoKeyException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public MissingCryptoKeyException(Throwable cause) {
+ super(cause);
+ }
+
+ protected MissingCryptoKeyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
new file mode 100644
index 0000000..0d1d5e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
@@ -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.registry.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class AESSensitivePropertyProviderFactoryTest extends GroovyTestCase {
+ private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactoryTest.class)
+
+ private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
+ private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider())
+
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testShouldGetProviderWithKey() throws Exception {
+ // Arrange
+ SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_128)
+
+ // Act
+ SensitivePropertyProvider provider = factory.getProvider()
+
+ // Assert
+ assert provider instanceof AESSensitivePropertyProvider
+ assert provider.@key
+ assert provider.@cipher
+ }
+
+ @Test
+ public void testShouldGetProviderWith256BitKey() throws Exception {
+ // Arrange
+ Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+ SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_256)
+
+ // Act
+ SensitivePropertyProvider provider = factory.getProvider()
+
+ // Assert
+ assert provider instanceof AESSensitivePropertyProvider
+ assert provider.@key
+ assert provider.@cipher
+ }
+}
\ No newline at end of file