http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy new file mode 100644 index 0000000..102ad9e --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy @@ -0,0 +1,103 @@ +/* + * 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.toolkit.encryptconfig.util + +import groovy.xml.XmlUtil +import org.apache.nifi.properties.SensitivePropertyProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.xml.sax.SAXException + +class NiFiRegistryAuthorizersXmlEncryptor extends XmlEncryptor { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryAuthorizersXmlEncryptor.class) + + static final String LDAP_USER_GROUP_PROVIDER_CLASS = "org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider" + private static final String LDAP_USER_GROUP_PROVIDER_REGEX = + /(?s)<userGroupProvider>(?:(?!<userGroupProvider>).)*?<class>\s*org\.apache\.nifi\.registry\.security\.ldap\.tenants\.LdapUserGroupProvider.*?<\/userGroupProvider>/ + /* Explanation of LDAP_USER_GROUP_PROVIDER_REGEX: + * (?s) -> single-line mode (i.e., `.` in regex matches newlines) + * <userGroupProvider> -> find occurrence of `<userGroupProvider>` literally (case-sensitive) + * (?: ... ) -> group but do not capture submatch + * (?! ... ) -> negative lookahead + * (?:(?!<userGroupProvider>).)*? -> find everything until a new `<userGroupProvider>` starts. This is for not selecting multiple userGroupProviders in one match + * <class> -> find occurrence of `<class>` literally (case-sensitive) + * \s* -> find any whitespace + * org\.apache\.nifi\.registry\.security\.ldap\.tenants\.LdapUserGroupProvider + * -> find occurrence of `org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider` literally (case-sensitive) + * .*?</userGroupProvider> -> find everything as needed up until and including occurrence of '</userGroupProvider>' + */ + + NiFiRegistryAuthorizersXmlEncryptor(SensitivePropertyProvider encryptionProvider, SensitivePropertyProvider decryptionProvider) { + super(encryptionProvider, decryptionProvider) + } + + /** + * Overrides the super class implementation to marking xml nodes that should be encrypted. + * This is done using logic specific to the authorizers.xml file type targeted by this subclass, + * leveraging knowledge of the XML file structure and which elements are sensitive. + * Sensitive nodes are marked by adding the encryption="none" attribute. + * When all the sensitive values are found and marked, the base class implementation + * is invoked to encrypt them. + * + * @param plainXmlContent the plaintext content of an authorizers.xml file + * @return the comment with sensitive values encrypted and marked with the cipher. + */ + @Override + String encrypt(String plainXmlContent) { + // First, mark the XML nodes to encrypt that are specific to authorizers.xml by adding an attribute encryption="none" + String markedXmlContent = markXmlNodesForEncryption(plainXmlContent, "userGroupProvider", { + it.find { + it.'class' as String == LDAP_USER_GROUP_PROVIDER_CLASS + }.property.findAll { + // Only operate on populated password properties + it.@name =~ "Password" && it.text() + } + }) + + // Now, return the results of the base implementation, which encrypts any node with an encryption="none" attribute + return super.encrypt(markedXmlContent) + } + + List<String> serializeXmlContentAndPreserveFormat(String updatedXmlContent, String originalXmlContent) { + if (updatedXmlContent == originalXmlContent) { + // If nothing was encrypted, e.g., the sensitive properties are commented out or empty, + // then the best thing to do to preserve formatting perspective is to do nothing. + return originalXmlContent.split("\n") + } + + // Find & replace the userGroupProvider element of the updated content in the original contents + try { + def parsedXml = new XmlSlurper().parseText(updatedXmlContent) + def provider = parsedXml.userGroupProvider.find { it.'class' as String == LDAP_USER_GROUP_PROVIDER_CLASS } + if (provider) { + def serializedProvider = new XmlUtil().serialize(provider) + // Remove XML declaration from top + serializedProvider = serializedProvider.replaceFirst(XML_DECLARATION_REGEX, "") + originalXmlContent = originalXmlContent.replaceFirst(LDAP_USER_GROUP_PROVIDER_REGEX, serializedProvider) + return originalXmlContent.split("\n") + } else { + throw new SAXException("No ldap-user-group-provider element found") + } + } catch (SAXException e) { + logger.warn("No userGroupProvider with class ${LDAP_USER_GROUP_PROVIDER_CLASS} found in XML content. " + + "The file could be empty or the element may be missing or commented out") + return originalXmlContent.split("\n") + } + } + +}
http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryIdentityProvidersXmlEncryptor.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryIdentityProvidersXmlEncryptor.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryIdentityProvidersXmlEncryptor.groovy new file mode 100644 index 0000000..fa6ce65 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryIdentityProvidersXmlEncryptor.groovy @@ -0,0 +1,102 @@ +/* + * 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.toolkit.encryptconfig.util + +import groovy.xml.XmlUtil +import org.apache.nifi.properties.SensitivePropertyProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.xml.sax.SAXException + +class NiFiRegistryIdentityProvidersXmlEncryptor extends XmlEncryptor { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryIdentityProvidersXmlEncryptor.class) + + static final String LDAP_PROVIDER_CLASS = "org.apache.nifi.registry.security.ldap.LdapIdentityProvider" + private static final String LDAP_PROVIDER_REGEX = /(?s)<provider>(?:(?!<provider>).)*?<class>\s*org\.apache\.nifi\.registry\.security\.ldap\.LdapIdentityProvider.*?<\/provider>/ + /* Explanation of LDAP_PROVIDER_REGEX: + * (?s) -> single-line mode (i.e., `.` in regex matches newlines) + * <provider> -> find occurrence of `<provider>` literally (case-sensitive) + * (?: ... ) -> group but do not capture submatch + * (?! ... ) -> negative lookahead + * (?:(?!<provider>).)*? -> find everything until a new `<provider>` starts. This is for not selecting multiple providers in one match + * <class> -> find occurrence of `<class>` literally (case-sensitive) + * \s* -> find any whitespace + * org\.apache\.nifi\.registry\.security\.ldap\.LdapIdentityProvider + * -> find occurrence of `org.apache.nifi.registry.security.ldap.LdapIdentityProvider` literally (case-sensitive) + * .*?</provider> -> find everything as needed up until and including occurrence of `</provider>` + */ + + NiFiRegistryIdentityProvidersXmlEncryptor(SensitivePropertyProvider encryptionProvider, SensitivePropertyProvider decryptionProvider) { + super(encryptionProvider, decryptionProvider) + } + + /** + * Overrides the super class implementation to marking xml nodes that should be encrypted. + * This is done using logic specific to the identity-providers.xml file type targeted by this + * subclass, leveraging knowledge of the XML file structure and which elements are sensitive. + * Sensitive nodes are marked by adding the encryption="none" attribute. + * When all the sensitive values are found and marked, the base class implementation + * is invoked to encrypt them. + * + * @param plainXmlContent the plaintext content of an identity-providers.xml file + * @return the comment with sensitive values encrypted and marked with the cipher. + */ + @Override + String encrypt(String plainXmlContent) { + // First, mark the XML nodes to encrypt that are specific to authorizers.xml by adding an attribute encryption="none" + String markedXmlContent = markXmlNodesForEncryption(plainXmlContent, "provider", { + it.find { + it.'class' as String == LDAP_PROVIDER_CLASS + }.property.findAll { + // Only operate on populated password properties + it.@name =~ "Password" && it.text() + } + }) + + // Now, return the results of the base implementation, which encrypts any node with an encryption="none" attribute + return super.encrypt(markedXmlContent) + } + + List<String> serializeXmlContentAndPreserveFormat(String updatedXmlContent, String originalXmlContent) { + if (updatedXmlContent == originalXmlContent) { + // If nothing was encrypted, e.g., the sensitive properties are commented out or empty, + // then the best thing to do to preserve formatting perspective is to do nothing. + return originalXmlContent.split("\n") + } + + // Find & replace the provider element of the updated content in the original contents + try { + def parsedXml = new XmlSlurper().parseText(updatedXmlContent) + def provider = parsedXml.provider.find { it.'class' as String == LDAP_PROVIDER_CLASS } + if (provider) { + def serializedProvider = new XmlUtil().serialize(provider) + // Remove XML declaration from top + serializedProvider = serializedProvider.replaceFirst(XML_DECLARATION_REGEX, "") + originalXmlContent = originalXmlContent.replaceFirst(LDAP_PROVIDER_REGEX, serializedProvider) + return originalXmlContent.split("\n") + } else { + throw new SAXException("No ldap-provider found") + } + } catch (SAXException e) { + logger.warn("No provider with class ${LDAP_PROVIDER_CLASS} found in XML content. " + + "The file could be empty or the element may be missing or commented out") + return originalXmlContent.split("\n") + } + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryPropertiesEncryptor.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryPropertiesEncryptor.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryPropertiesEncryptor.groovy new file mode 100644 index 0000000..5ea8d7b --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryPropertiesEncryptor.groovy @@ -0,0 +1,65 @@ +/* + * 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.toolkit.encryptconfig.util + +import org.apache.nifi.properties.SensitivePropertyProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.regex.Pattern + +class NiFiRegistryPropertiesEncryptor extends PropertiesEncryptor { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesEncryptor.class) + + // TODO, if and when we add a dependency on NiFi Registry, we can import these dependencies array rather than redefining them + + // Defined in nifi-registry-properties: org.apache.nifi.registry.properties.NiFiRegistryProperties + private static final String SECURITY_KEYSTORE_PASSWD = "nifi.registry.security.keystorePasswd" + private static final String SECURITY_KEY_PASSWD = "nifi.registry.security.keyPasswd" + private static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.registry.security.truststorePasswd" + + // Defined in nifi-registry-properties: org.apache.nifi.registry.properties.ProtectedNiFiRegistryProperties + private static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = "nifi.registry.sensitive.props.additional.keys" + private static final String[] DEFAULT_SENSITIVE_PROPERTIES = [ + SECURITY_KEYSTORE_PASSWD, + SECURITY_KEY_PASSWD, + SECURITY_TRUSTSTORE_PASSWD + ] + + NiFiRegistryPropertiesEncryptor(SensitivePropertyProvider encryptionProvider, SensitivePropertyProvider decryptionProvider) { + super(encryptionProvider, decryptionProvider) + } + + @Override + Properties encrypt(Properties properties) { + Set<String> propertiesToEncrypt = new HashSet<>() + propertiesToEncrypt.addAll(DEFAULT_SENSITIVE_PROPERTIES) + propertiesToEncrypt.addAll(getAdditionalSensitivePropertyKeys(properties)) + + return encrypt(properties, propertiesToEncrypt) + } + + private static String[] getAdditionalSensitivePropertyKeys(Properties properties) { + String rawAdditionalSensitivePropertyKeys = properties.getProperty(ADDITIONAL_SENSITIVE_PROPERTIES_KEY) + if (!rawAdditionalSensitivePropertyKeys) { + return [] + } + return rawAdditionalSensitivePropertyKeys.split(Pattern.quote(",")) + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/PropertiesEncryptor.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/PropertiesEncryptor.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/PropertiesEncryptor.groovy new file mode 100644 index 0000000..189e9a5 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/PropertiesEncryptor.groovy @@ -0,0 +1,269 @@ +/* + * 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.toolkit.encryptconfig.util + +import groovy.io.GroovyPrintWriter +import org.apache.commons.configuration2.PropertiesConfiguration +import org.apache.commons.configuration2.PropertiesConfigurationLayout +import org.apache.commons.configuration2.builder.fluent.Configurations +import org.apache.nifi.properties.SensitivePropertyProvider +import org.apache.nifi.util.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.regex.Pattern + +class PropertiesEncryptor { + + private static final Logger logger = LoggerFactory.getLogger(PropertiesEncryptor.class) + + private static final String SUPPORTED_PROPERTY_FILE_REGEX = /^\s*nifi\.[-.\w\s]+\s*=/ + protected static final String PROPERTY_PART_DELIMINATOR = "." + protected static final String PROTECTION_ID_PROPERTY_SUFFIX = "protected" + + protected SensitivePropertyProvider encryptionProvider + protected SensitivePropertyProvider decryptionProvider + + PropertiesEncryptor(SensitivePropertyProvider encryptionProvider, SensitivePropertyProvider decryptionProvider) { + this.encryptionProvider = encryptionProvider + this.decryptionProvider = decryptionProvider + } + + static boolean supportsFile(String filePath) { + try { + File file = new File(filePath) + if (!ToolUtilities.canRead(file)) { + return false + } + Pattern p = Pattern.compile(SUPPORTED_PROPERTY_FILE_REGEX); + return file.readLines().any { it =~ SUPPORTED_PROPERTY_FILE_REGEX } + } catch (Throwable ignored) { + return false + } + } + + static Properties loadFile(String filePath) throws IOException { + + Properties rawProperties + File inputPropertiesFile = new File(filePath) + + if (ToolUtilities.canRead(inputPropertiesFile)) { + rawProperties = new Properties() + inputPropertiesFile.withReader { reader -> + rawProperties.load(reader) + } + } else { + throw new IOException("The file at ${filePath} must exist and be readable by the user running this tool") + } + + return rawProperties + + } + + Properties decrypt(final Properties properties) { + + Set<String> propertiesToSkip = getProtectionIdPropertyKeys(properties) + Map<String, String> propertiesToDecrypt = getProtectedPropertyKeys(properties) + + if (propertiesToDecrypt.isEmpty()) { + return properties + } + + if (decryptionProvider == null) { + throw new IllegalStateException("Decryption capability not supported without provider. " + + "Usually this means a decryption password / key was not provided to the tool.") + } + + String supportedDecryptionScheme = decryptionProvider.getIdentifierKey() + if (supportedDecryptionScheme) { + propertiesToDecrypt.entrySet().each { entry -> + if (!supportedDecryptionScheme.equals(entry.getValue())) { + throw new IllegalStateException("Decryption capability not supported by this tool. " + + "This tool supports ${supportedDecryptionScheme}, but this properties file contains " + + "${entry.getKey()} protected by ${entry.getValue()}") + } + } + } + + Properties unprotectedProperties = new Properties() + + for (String propertyName : properties.stringPropertyNames()) { + String propertyValue = properties.getProperty(propertyName) + if (propertiesToSkip.contains(propertyName)) { + continue + } + if (propertiesToDecrypt.keySet().contains(propertyName)) { + String decryptedPropertyValue = decryptionProvider.unprotect(propertyValue) + unprotectedProperties.setProperty(propertyName, decryptedPropertyValue) + } else { + unprotectedProperties.setProperty(propertyName, propertyValue) + } + } + + return unprotectedProperties + } + + Properties encrypt(Properties properties) { + return encrypt(properties, properties.stringPropertyNames()) + } + + Properties encrypt(final Properties properties, final Set<String> propertiesToEncrypt) { + + if (encryptionProvider == null) { + throw new IllegalStateException("Input properties is encrypted, but decryption capability is not enabled. " + + "Usually this means a decryption password / key was not provided to the tool.") + } + + logger.debug("Encrypting ${propertiesToEncrypt.size()} properties") + + Properties protectedProperties = new Properties(); + for (String propertyName : properties.stringPropertyNames()) { + String propertyValue = properties.getProperty(propertyName) + // empty properties are not encrypted + if (!StringUtils.isEmpty(propertyValue) && propertiesToEncrypt.contains(propertyName)) { + String encryptedPropertyValue = encryptionProvider.protect(propertyValue) + protectedProperties.setProperty(propertyName, encryptedPropertyValue) + protectedProperties.setProperty(protectionPropertyForProperty(propertyName), encryptionProvider.getIdentifierKey()) + } else { + protectedProperties.setProperty(propertyName, propertyValue) + } + } + + return protectedProperties + } + + void write(Properties updatedProperties, String outputFilePath, String inputFilePath) { + if (!outputFilePath) { + throw new IllegalArgumentException("Cannot write encrypted properties to empty file path") + } + File outputPropertiesFile = new File(outputFilePath) + + if (ToolUtilities.isSafeToWrite(outputPropertiesFile)) { + String serializedProperties = serializePropertiesAndPreserveFormatIfPossible(updatedProperties, inputFilePath) + outputPropertiesFile.text = serializedProperties + } else { + throw new IOException("The file at ${outputFilePath} must be writable by the user running this tool") + } + } + + private String serializePropertiesAndPreserveFormatIfPossible(Properties updatedProperties, String inputFilePath) { + List<String> linesToPersist + File inputPropertiesFile = new File(inputFilePath) + if (ToolUtilities.canRead(inputPropertiesFile)) { + // Instead of just writing the Properties instance to a properties file, + // this method attempts to maintain the structure of the original file and preserves comments + linesToPersist = serializePropertiesAndPreserveFormat(updatedProperties, inputPropertiesFile) + } else { + linesToPersist = serializeProperties(updatedProperties) + } + return linesToPersist.join("\n") + } + + private List<String> serializePropertiesAndPreserveFormat(Properties properties, File originalPropertiesFile) { + Configurations configurations = new Configurations() + try { + PropertiesConfiguration originalPropertiesConfiguration = configurations.properties(originalPropertiesFile) + def keysToAdd = properties.keySet().findAll { !originalPropertiesConfiguration.containsKey(it.toString()) } + def keysToUpdate = properties.keySet().findAll { + !keysToAdd.contains(it) && + properties.getProperty(it.toString()) != originalPropertiesConfiguration.getProperty(it.toString()) + } + def keysToRemove = originalPropertiesConfiguration.getKeys().findAll {!properties.containsKey(it) } + + keysToUpdate.forEach { + originalPropertiesConfiguration.setProperty(it.toString(), properties.getProperty(it.toString())) + } + keysToRemove.forEach { + originalPropertiesConfiguration.clearProperty(it.toString()) + } + boolean isFirst = true + keysToAdd.sort().forEach { + originalPropertiesConfiguration.setProperty(it.toString(), properties.getProperty(it.toString())) + if (isFirst) { + originalPropertiesConfiguration.getLayout().setBlancLinesBefore(it.toString(), 1) + originalPropertiesConfiguration.getLayout().setComment(it.toString(), "protection properties") + isFirst = false + } + } + + OutputStream out = new ByteArrayOutputStream() + Writer writer = new GroovyPrintWriter(out) + + PropertiesConfigurationLayout layout = originalPropertiesConfiguration.getLayout() + layout.setGlobalSeparator("=") + layout.save(originalPropertiesConfiguration, writer) + + writer.flush() + List<String> lines = out.toString().split("\n") + + return lines + } catch(Exception e) { + throw new RuntimeException("Error serializing properties.", e) + } + } + + private List<String> serializeProperties(final Properties properties) { + OutputStream out = new ByteArrayOutputStream() + Writer writer = new GroovyPrintWriter(out) + + properties.store(writer, null) + writer.flush() + List<String> lines = out.toString().split("\n") + + return lines + } + + /** + * Returns a Map of the keys identifying properties that are currently protected + * and the protection identifier for each. The protection + * + * @return the Map of protected property keys and the protection identifier for each + */ + private static Map<String, String> getProtectedPropertyKeys(Properties properties) { + Map<String, String> protectedProperties = new HashMap<>(); + properties.stringPropertyNames().forEach({ key -> + String protectionKey = protectionPropertyForProperty(key) + String protectionIdentifier = properties.getProperty(protectionKey) + if (protectionIdentifier) { + protectedProperties.put(key, protectionIdentifier) + } + }) + return protectedProperties + } + + private static Set<String> getProtectionIdPropertyKeys(Properties properties) { + Set<String> protectedProperties = properties.stringPropertyNames().findAll { key -> + key.endsWith(PROPERTY_PART_DELIMINATOR + PROTECTION_ID_PROPERTY_SUFFIX) + } + return protectedProperties; + } + + private static String protectionPropertyForProperty(String propertyName) { + return propertyName + PROPERTY_PART_DELIMINATOR + PROTECTION_ID_PROPERTY_SUFFIX + } + + private static String propertyForProtectionProperty(String protectionPropertyName) { + String[] propertyNameParts = protectionPropertyName.split(Pattern.quote(PROPERTY_PART_DELIMINATOR)) + if (propertyNameParts.length >= 2 && PROTECTION_ID_PROPERTY_SUFFIX.equals(propertyNameParts[-1])) { + return propertyNameParts[(0..-2)].join(PROPERTY_PART_DELIMINATOR) + } + return null + } + + + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/ToolUtilities.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/ToolUtilities.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/ToolUtilities.groovy new file mode 100644 index 0000000..4e8c1b7 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/ToolUtilities.groovy @@ -0,0 +1,164 @@ +/* + * 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.toolkit.encryptconfig.util + +import org.apache.commons.cli.CommandLine +import org.apache.commons.codec.binary.Hex +import org.apache.nifi.util.console.TextDevice +import org.apache.nifi.util.console.TextDevices +import org.bouncycastle.crypto.generators.SCrypt +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.crypto.Cipher +import java.nio.charset.StandardCharsets +import java.security.KeyException + +class ToolUtilities { + + private static final Logger logger = LoggerFactory.getLogger(ToolUtilities.class) + + private static final int DEFAULT_MIN_PASSWORD_LENGTH = 12 + + // Strong parameters as of 12 Aug 2016 + private static final int SCRYPT_N = 2**16 + private static final int SCRYPT_R = 8 + private static final int SCRYPT_P = 1 + + static boolean isExactlyOneOptionSet(CommandLine commandLine, String... opt) { + Collection<Boolean> setOptions = opt.findAll{commandLine.hasOption(it)} + return setOptions.size() == 1 + } + + static boolean isExactlyOneTrue(Boolean... b) { + Collection<Boolean> trues = b.findAll{it} + return trues.size() == 1 + } + + /** + * Helper method which returns true if the provided file exists and is readable + * + * @param fileToRead the proposed file to read + * @return true if the caller should be able to successfully read from this file + */ + static boolean canRead(File fileToRead) { + fileToRead && (fileToRead.exists() && fileToRead.canRead()) + } + + /** + * Helper method which returns true if it is "safe" to write to the provided file. + * + * Conditions: + * file does not exist and the parent directory is writable + * -OR- + * file exists and is writable + * + * @param fileToWrite the proposed file to be written to + * @return true if the caller can "safely" write to this file location + */ + static boolean isSafeToWrite(File fileToWrite) { + fileToWrite && ((!fileToWrite.exists() && fileToWrite.absoluteFile.parentFile.canWrite()) || (fileToWrite.exists() && fileToWrite.canWrite())) + } + + + /** + * The method returns the provided, derived, or securely-entered key in hex format. + * + * @param device + * @param keyHex + * @param password + * @param usingPassword + * @return + */ + public static String determineKey(TextDevice device = TextDevices.defaultTextDevice(), String keyHex, String password, boolean usingPassword) { + if (usingPassword) { + if (!password) { + logger.debug("Reading password from secure console") + password = readPasswordFromConsole(device) + } + keyHex = deriveKeyFromPassword(password) + password = null + return keyHex + } else { + if (!keyHex) { + logger.debug("Reading hex key from secure console") + keyHex = readKeyFromConsole(device) + } + return keyHex + } + } + + private static String readKeyFromConsole(TextDevice textDevice) { + textDevice.printf("Enter the master key in hexadecimal format (spaces acceptable): ") + new String(textDevice.readPassword()) + } + + private static String readPasswordFromConsole(TextDevice textDevice) { + textDevice.printf("Enter the password: ") + new String(textDevice.readPassword()) + } + +// /** +// * Returns the key in uppercase hexadecimal format with delimiters (spaces, '-', etc.) removed. All non-hex chars are removed. If the result is not a valid length (32, 48, 64 chars depending on the JCE), an exception is thrown. +// * +// * @param rawKey the unprocessed key input +// * @return the formatted hex string in uppercase +// * @throws java.security.KeyException if the key is not a valid length after parsing +// */ +// public static String parseKey(String rawKey) throws KeyException { +// String hexKey = rawKey.replaceAll("[^0-9a-fA-F]", "") +// def validKeyLengths = getValidKeyLengths() +// if (!validKeyLengths.contains(hexKey.size() * 4)) { +// throw new KeyException("The key (${hexKey.size()} hex chars) must be of length ${validKeyLengths} bits (${validKeyLengths.collect { it / 4 }} hex characters)") +// } +// hexKey.toUpperCase() +// } + + /** + * Returns the list of acceptable key lengths in bits based on the current JCE policies. + * + * @return 128 , [192, 256] + */ + public static List<Integer> getValidKeyLengths() { + Cipher.getMaxAllowedKeyLength("AES") > 128 ? [128, 192, 256] : [128] + } + + private static String deriveKeyFromPassword(String password, int minPasswordLength = DEFAULT_MIN_PASSWORD_LENGTH) { + password = password?.trim() + if (!password || password.length() < minPasswordLength) { + throw new KeyException("Cannot derive key from empty/short password -- password must be at least ${minPasswordLength} characters") + } + + // Generate a 128 bit salt + byte[] salt = generateScryptSalt() + int keyLengthInBytes = getValidKeyLengths().max() / 8 + byte[] derivedKeyBytes = SCrypt.generate(password.getBytes(StandardCharsets.UTF_8), salt, SCRYPT_N, SCRYPT_R, SCRYPT_P, keyLengthInBytes) + Hex.encodeHexString(derivedKeyBytes).toUpperCase() + } + + private static byte[] generateScryptSalt() { +// byte[] salt = new byte[16] +// new SecureRandom().nextBytes(salt) +// salt + /* It is not ideal to use a static salt, but the KDF operation must be deterministic + for a given password, and storing and retrieving the salt in bootstrap.conf causes + compatibility concerns + */ + "NIFI_SCRYPT_SALT".getBytes(StandardCharsets.UTF_8) + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/XmlEncryptor.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/XmlEncryptor.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/XmlEncryptor.groovy new file mode 100644 index 0000000..8324682 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/XmlEncryptor.groovy @@ -0,0 +1,200 @@ +/* + * 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.toolkit.encryptconfig.util + +import groovy.util.slurpersupport.GPathResult +import groovy.xml.XmlUtil +import org.apache.nifi.properties.SensitivePropertyProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +abstract class XmlEncryptor { + + protected static final String XML_DECLARATION_REGEX = /<\?xml version="1.0" encoding="UTF-8"\?>/ + protected static final ENCRYPTION_NONE = "none" + protected static final ENCRYPTION_EMPTY = "" + + private static final Logger logger = LoggerFactory.getLogger(XmlEncryptor.class) + + protected SensitivePropertyProvider decryptionProvider + protected SensitivePropertyProvider encryptionProvider + + XmlEncryptor(SensitivePropertyProvider encryptionProvider, SensitivePropertyProvider decryptionProvider) { + this.decryptionProvider = decryptionProvider + this.encryptionProvider = encryptionProvider + } + + static boolean supportsFile(String filePath) { + def doc + try { + String rawFileContents = loadXmlFile(filePath) + doc = new XmlSlurper().parseText(rawFileContents) + } catch (Throwable ignored) { + return false + } + return doc != null + } + + static String loadXmlFile(String xmlFilePath) throws IOException { + File xmlFile = new File(xmlFilePath) + if (ToolUtilities.canRead(xmlFile)) { + try { + String xmlContent = xmlFile.text + return xmlContent + } catch (RuntimeException e) { + throw new IOException("Cannot load XML from ${xmlFilePath}", e) + } + } else { + throw new IOException("File at ${xmlFilePath} must exist and be readable by user running this tool.") + } + } + + String decrypt(String encryptedXmlContent) { + try { + + def doc = new XmlSlurper().parseText(encryptedXmlContent) + GPathResult[] encryptedNodes = doc.depthFirst().findAll { GPathResult node -> + node.@encryption != ENCRYPTION_NONE && node.@encryption != ENCRYPTION_EMPTY + } + + if (encryptedNodes.size() == 0) { + return encryptedXmlContent + } + + if (decryptionProvider == null) { + throw new IllegalStateException("Input XML is encrypted, but decryption capability is not enabled. " + + "Usually this means a decryption password / key was not provided to the tool.") + } + String supportedDecryptionScheme = decryptionProvider.getIdentifierKey() + + logger.debug("Found ${encryptedNodes.size()} encrypted XML elements. Will attempt to decrypt using the provided decryption key.") + + encryptedNodes.each { node -> + logger.debug("Attempting to decrypt ${node.text()}") + if (node.@encryption != supportedDecryptionScheme) { + throw new IllegalStateException("Decryption capability not supported by this tool. " + + "This tool supports ${supportedDecryptionScheme}, but this xml file contains " + + "${node.toString()} protected by ${node.@encryption}") + } + String decryptedValue = decryptionProvider.unprotect(node.text().trim()) + node.@encryption = ENCRYPTION_NONE + node.replaceBody(decryptedValue) + } + + // Does not preserve whitespace formatting or comments + String updatedXml = XmlUtil.serialize(doc) + logger.debug("Updated XML content: ${updatedXml}") + return updatedXml + + } catch (Exception e) { + throw new RuntimeException("Cannot decrypt XML content", e) + } + } + + String encrypt(String plainXmlContent) { + try { + def doc = new XmlSlurper().parseText(plainXmlContent) + + GPathResult[] nodesToEncrypt = doc.depthFirst().findAll { GPathResult node -> + node.text() && node.@encryption == ENCRYPTION_NONE + } + + logger.debug("Encrypting ${nodesToEncrypt.size()} element(s) of XML decoument") + + if (nodesToEncrypt.size() == 0) { + return plainXmlContent + } + + nodesToEncrypt.each { node -> + String encryptedValue = this.encryptionProvider.protect(node.text().trim()) + node.@encryption = this.encryptionProvider.getIdentifierKey() + node.replaceBody(encryptedValue) + } + + // Does not preserve whitespace formatting or comments + String updatedXml = XmlUtil.serialize(doc) + logger.debug("Updated XML content: ${updatedXml}") + return updatedXml + } catch (Exception e) { + throw new RuntimeException("Cannot encrypt XML content", e) + } + } + + void writeXmlFile(String updatedXmlContent, String outputXmlPath, String inputXmlPath) throws IOException { + File outputXmlFile = new File(outputXmlPath) + if (ToolUtilities.isSafeToWrite(outputXmlFile)) { + String finalXmlContent = serializeXmlContentAndPreserveFormatIfPossible(updatedXmlContent, inputXmlPath) + outputXmlFile.text = finalXmlContent + } else { + throw new IOException("The XML file at ${outputXmlPath} must be writable by the user running this tool") + } + } + + String serializeXmlContentAndPreserveFormatIfPossible(String updatedXmlContent, String inputXmlPath) { + String finalXmlContent + File inputXmlFile = new File(inputXmlPath) + if (ToolUtilities.canRead(inputXmlFile)) { + String originalXmlContent = new File(inputXmlPath).text + // Instead of just writing the XML content to a file, this method attempts to maintain + // the structure of the original file. + finalXmlContent = serializeXmlContentAndPreserveFormat(updatedXmlContent, originalXmlContent).join("\n") + } else { + finalXmlContent = updatedXmlContent + } + return finalXmlContent + } + + /** + * Given an original XML file and updated XML content, create the lines for an updated, minimally altered, serialization. + * Concrete classes extending this class must implement this method using specific knowledge of the XML document. + * + * @param finalXmlContent the xml content to serialize + * @param inputXmlFile the original input xml file to use as a template for formatting the serialization + * @return the lines of serialized finalXmlContent that are close in raw format to originalInputXmlFile + */ + abstract List<String> serializeXmlContentAndPreserveFormat(String updatedXmlContent, String originalXmlContent) + // TODO, replace the above abstract method with an implementation that works generically for any updated (encryption=."..") nodes + // perhaps this could be done leveraging org.apache.commons.configuration2 which is capable of preserving comments, eg: + + + static String markXmlNodesForEncryption(String plainXmlContent, String gPath, gPathCallback) { + String markedXmlContent + try { + def doc = new XmlSlurper().parseText(plainXmlContent) + // Find the provider element by class even if it has been renamed + def sensitiveProperties = gPathCallback(doc."${gPath}") + + logger.debug("Marking ${sensitiveProperties.size()} sensitive element(s) of XML to be encrypted") + + if (sensitiveProperties.size() == 0) { + logger.debug("No populated sensitive properties found in XML content") + return plainXmlContent + } + + sensitiveProperties.each { + it.@encryption = ENCRYPTION_NONE + } + + // Does not preserve whitespace formatting or comments + // TODO: Switch to XmlParser & XmlNodePrinter to maintain "empty" element structure + markedXmlContent = XmlUtil.serialize(doc) + } catch (Exception e) { + logger.debug("Encountered exception", e) + throw new RuntimeException(e) + } + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties index fc2aaf1..9a13479 100644 --- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties @@ -18,5 +18,6 @@ log4j.rootLogger=INFO,console log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.Target=System.err log4j.appender.console.layout=org.apache.log4j.PatternLayout -log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n \ No newline at end of file +log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p %c{1}: %m%n http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/EncryptConfigMainTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/EncryptConfigMainTest.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/EncryptConfigMainTest.groovy new file mode 100644 index 0000000..b2b8040 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/EncryptConfigMainTest.groovy @@ -0,0 +1,285 @@ +/* + * 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.toolkit.encryptconfig + +import org.apache.nifi.properties.AESSensitivePropertyProvider +import org.apache.nifi.properties.ConfigEncryptionTool +import org.apache.nifi.properties.NiFiPropertiesLoader +import org.apache.nifi.toolkit.encryptconfig.util.BootstrapUtil +import org.apache.nifi.util.NiFiProperties +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.contrib.java.lang.system.Assertion +import org.junit.contrib.java.lang.system.ExpectedSystemExit +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Files +import java.security.Security + +import static org.apache.nifi.toolkit.encryptconfig.TestUtil.* + +@RunWith(JUnit4.class) +class EncryptConfigMainTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(EncryptConfigMainTest.class) + + @Rule + public final ExpectedSystemExit exit = ExpectedSystemExit.none() + + @BeforeClass + static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + + setupTmpDir() + } + + @Test + void testDetermineModeFromArgsWithLegacyMode() { + // Arrange + def argsList = "-b conf/bootstrap.conf -n conf/nifi.properties".split(" ").toList() + + // Act + def toolMode = EncryptConfigMain.determineModeFromArgs(argsList) + + // Assert + toolMode != null + toolMode instanceof LegacyMode + } + + @Test + void testDetermineModeFromArgsWithNifiRegistryMode() { + // Arrange + def argsList = "--nifiRegistry".split(" ").toList() + // Act + def toolMode = EncryptConfigMain.determineModeFromArgs(argsList) + // Assert + toolMode != null + toolMode instanceof NiFiRegistryMode + !argsList.contains("--nifiRegistry") + + /* Test when --nifiRegistry is not first flag */ + + // Arrange + argsList = "-b conf/bootstrap.conf -p --nifiRegistry -r conf/nifi-registry.properties".split(" ").toList() + // Act + toolMode = EncryptConfigMain.determineModeFromArgs(argsList) + // Assert + toolMode != null + toolMode instanceof NiFiRegistryMode + !argsList.contains("--nifiRegistry") + } + + @Test + void testDetermineModeFromArgsWithNifiRegistryDecryptMode() { + // Arrange + def argsList = "--nifiRegistry --decrypt".split(" ").toList() + // Act + def toolMode = EncryptConfigMain.determineModeFromArgs(argsList) + // Assert + toolMode != null + toolMode instanceof NiFiRegistryDecryptMode + !argsList.contains("--nifiRegistry") + !argsList.contains("--decrypt") + + /* Test when --decrypt comes before --nifiRegistry */ + + // Arrange + argsList = "--b conf/bootstrap.conf --decrypt --nifiRegistry".split(" ").toList() + // Act + toolMode = EncryptConfigMain.determineModeFromArgs(argsList) + // Assert + toolMode != null + toolMode instanceof NiFiRegistryDecryptMode + !argsList.contains("--nifiRegistry") + !argsList.contains("--decrypt") + } + + @Test + void testDetermineModeFromArgsReturnsNullOnDecryptWithoutNifiRegistryPresent() { + // Arrange + def argsList = "--decrypt".split(" ").toList() + + // Act + def toolMode = EncryptConfigMain.determineModeFromArgs(argsList) + + // Assert + toolMode == null + } + + @Test + void testShouldPerformFullOperationForNiFiPropertiesAndLoginIdentityProvidersAndAuthorizers() { + // Arrange + exit.expectSystemExitWithStatus(0) + + File tmpDir = setupTmpDir() + + File emptyKeyFile = new File("src/test/resources/bootstrap_with_empty_master_key.conf") + File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf") + bootstrapFile.delete() + + Files.copy(emptyKeyFile.toPath(), bootstrapFile.toPath()) + final List<String> originalBootstrapLines = bootstrapFile.readLines() + String originalKeyLine = originalBootstrapLines.find { + it.startsWith("${BootstrapUtil.NIFI_BOOTSTRAP_KEY_PROPERTY}=") + } + logger.info("Original key line from bootstrap.conf: ${originalKeyLine}") + assert originalKeyLine == "${BootstrapUtil.NIFI_BOOTSTRAP_KEY_PROPERTY}=" + + final String EXPECTED_KEY_LINE = "${BootstrapUtil.NIFI_BOOTSTRAP_KEY_PROPERTY}=${KEY_HEX}" + + // Set up the NFP file + File inputPropertiesFile = new File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties") + File outputPropertiesFile = new File("target/tmp/tmp_nifi.properties") + outputPropertiesFile.delete() + + NiFiProperties inputProperties = new NiFiPropertiesLoader().load(inputPropertiesFile) + logger.info("Loaded ${inputProperties.size()} properties from input file") + + // Set up the LIP file + File inputLIPFile = new File("src/test/resources/login-identity-providers-populated.xml") + File outputLIPFile = new File("target/tmp/tmp-lip.xml") + outputLIPFile.delete() + + String originalLipXmlContent = inputLIPFile.text + logger.info("Original LIP XML content: ${originalLipXmlContent}") + + // Set up the Authorizers file + File inputAuthorizersFile = new File("src/test/resources/authorizers-populated.xml") + File outputAuthorizersFile = new File("target/tmp/tmp-authorizers.xml") + outputAuthorizersFile.delete() + + String originalAuthorizersXmlContent = inputAuthorizersFile.text + logger.info("Original Authorizers XML content: ${originalAuthorizersXmlContent}") + + String[] args = [ + "-n", inputPropertiesFile.path, + "-l", inputLIPFile.path, + "-a", inputAuthorizersFile.path, + "-b", bootstrapFile.path, + "-o", outputPropertiesFile.path, + "-i", outputLIPFile.path, + "-u", outputAuthorizersFile.path, + "-k", KEY_HEX, + "-v"] + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + exit.checkAssertionAfterwards(new Assertion() { + void checkAssertion() { + + /*** NiFi Properties Assertions ***/ + + final List<String> updatedPropertiesLines = outputPropertiesFile.readLines() + logger.info("Updated nifi.properties:") + logger.info("\n" * 2 + updatedPropertiesLines.join("\n")) + + // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) + NiFiProperties updatedProperties = new NiFiPropertiesLoader().readProtectedPropertiesFromDisk(outputPropertiesFile) + assert updatedProperties.size() >= inputProperties.size() + + // Check that the new NiFiProperties instance matches the output file (values still encrypted) + updatedProperties.getPropertyKeys().every { String key -> + assert updatedPropertiesLines.contains("${key}=${updatedProperties.getProperty(key)}".toString()) + } + + /*** Login Identity Providers Assertions ***/ + + final String updatedLipXmlContent = outputLIPFile.text + logger.info("Updated LIP XML content: ${updatedLipXmlContent}") + // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) + def originalLipParsedXml = new XmlSlurper().parseText(originalLipXmlContent) + def updatedLipParsedXml = new XmlSlurper().parseText(updatedLipXmlContent) + assert originalLipParsedXml != updatedLipParsedXml + assert originalLipParsedXml.'**'.findAll { it.@encryption } != updatedLipParsedXml.'**'.findAll { + it.@encryption + } + def lipEncryptedValues = updatedLipParsedXml.provider.find { + it.identifier == 'ldap-provider' + }.property.findAll { + it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}" + } + lipEncryptedValues.each { + assert spp.unprotect(it.text()) == PASSWORD + } + // Check that the comments are still there + def lipTrimmedLines = inputLIPFile.readLines().collect { it.trim() }.findAll { it } + def lipTrimmedSerializedLines = updatedLipXmlContent.split("\n").collect { it.trim() }.findAll { it } + assert lipTrimmedLines.size() == lipTrimmedSerializedLines.size() + + /*** Authorizers Assertions ***/ + + final String updatedAuthorizersXmlContent = outputAuthorizersFile.text + logger.info("Updated Authorizers XML content: ${updatedAuthorizersXmlContent}") + // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) + def originalAuthorizersParsedXml = new XmlSlurper().parseText(originalAuthorizersXmlContent) + def updatedAuthorizersParsedXml = new XmlSlurper().parseText(updatedAuthorizersXmlContent) + assert originalAuthorizersParsedXml != updatedAuthorizersParsedXml + assert originalAuthorizersParsedXml.'**'.findAll { it.@encryption } != updatedAuthorizersParsedXml.'**'.findAll { + it.@encryption + } + def authorizersEncryptedValues = updatedAuthorizersParsedXml.userGroupProvider.find { + it.identifier == 'ldap-user-group-provider' + }.property.findAll { + it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}" + } + authorizersEncryptedValues.each { + assert spp.unprotect(it.text()) == PASSWORD + } + // Check that the comments are still there + def authorizersTrimmedLines = inputAuthorizersFile.readLines().collect { it.trim() }.findAll { it } + def authorizersTrimmedSerializedLines = updatedAuthorizersXmlContent.split("\n").collect { it.trim() }.findAll { it } + assert authorizersTrimmedLines.size() == authorizersTrimmedSerializedLines.size() + + /*** Bootstrap assertions ***/ + + // Check that the key was persisted to the bootstrap.conf + final List<String> updatedBootstrapLines = bootstrapFile.readLines() + String updatedKeyLine = updatedBootstrapLines.find { + it.startsWith(BootstrapUtil.NIFI_BOOTSTRAP_KEY_PROPERTY) + } + logger.info("Updated key line: ${updatedKeyLine}") + + assert updatedKeyLine == EXPECTED_KEY_LINE + assert originalBootstrapLines.size() == updatedBootstrapLines.size() + + // Clean up + outputPropertiesFile.deleteOnExit() + outputLIPFile.deleteOnExit() + outputAuthorizersFile.deleteOnExit() + bootstrapFile.deleteOnExit() + tmpDir.deleteOnExit() + } + }) + + // Act + EncryptConfigMain.main(args) + logger.info("Invoked #main with ${args.join(" ")}") + + // Assert + + // Assertions defined above + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptModeSpec.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptModeSpec.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptModeSpec.groovy new file mode 100644 index 0000000..2ff1414 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptModeSpec.groovy @@ -0,0 +1,117 @@ +/* + * 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.toolkit.encryptconfig + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.Assume +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Specification + +import java.security.Security + +import static org.apache.nifi.toolkit.encryptconfig.TestUtil.* + +class NiFiRegistryDecryptModeSpec extends Specification { + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryDecryptModeSpec.class) + + ByteArrayOutputStream toolStdOutContent + PrintStream origSystemOut + + // runs before every feature method + def setup() { + origSystemOut = System.out + toolStdOutContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(toolStdOutContent)); + } + + // runs after every feature method + def cleanup() { + toolStdOutContent.flush() + System.setOut(origSystemOut); + toolStdOutContent.close() + } + + // runs before the first feature method + def setupSpec() { + Security.addProvider(new BouncyCastleProvider()) + setupTmpDir() + } + + // runs after the last feature method + def cleanupSpec() { + cleanupTmpDir() + } + + def "decrypt protected nifi-registry.properties file using -k"() { + + setup: + NiFiRegistryDecryptMode tool = new NiFiRegistryDecryptMode() + def inRegistryProperties1 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_KEY_128) + File outRegistryProperties1 = generateTmpFile() + + when: "run with args: -k <key> -r <file>" + tool.run("-k ${KEY_HEX_128} -r ${inRegistryProperties1}".split(" ")) + toolStdOutContent.flush() + outRegistryProperties1.text = toolStdOutContent.toString() + then: "decrypted properties file was printed to std out" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED, outRegistryProperties1.getAbsolutePath(), true) + and: "input properties file is still encrypted" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_KEY_128, inRegistryProperties1, true) + + } + + def "decrypt protected nifi-registry.properties file using -p [256-bit]"() { + + Assume.assumeTrue("Test only runs when unlimited strength crypto is available", isUnlimitedStrengthCryptoAvailable()) + + setup: + NiFiRegistryDecryptMode tool = new NiFiRegistryDecryptMode() + def inRegistryProperties1 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_PASSWORD_256) + File outRegistryProperties1 = generateTmpFile() + + when: "run with args: -p <password> -r <file>" + tool.run("-p ${PASSWORD} -r ${inRegistryProperties1}".split(" ")) + toolStdOutContent.flush() + outRegistryProperties1.text = toolStdOutContent.toString() + then: "decrypted properties file was printed to std out" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED, outRegistryProperties1.getAbsolutePath(), true) + and: "input properties file is still encrypted" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_PASSWORD_256, inRegistryProperties1, true) + + } + + def "decrypt protected nifi-registry.properties file using -b"() { + + setup: + NiFiRegistryDecryptMode tool = new NiFiRegistryDecryptMode() + def inRegistryProperties = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_KEY_128) + def inBootstrap = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_KEY_128) + File outRegistryProperties = generateTmpFile() + + when: "run with args: -b <file> -r <file>" + tool.run("-b ${inBootstrap} -r ${inRegistryProperties}".split(" ")) + toolStdOutContent.flush() + outRegistryProperties.text = toolStdOutContent.toString() + then: "decrypted properties file was printed to std out" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED, outRegistryProperties.getAbsolutePath(), true) + and: "input properties file is still encrypted" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_KEY_128, inRegistryProperties, true) + + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a8817e02/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryModeSpec.groovy ---------------------------------------------------------------------- diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryModeSpec.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryModeSpec.groovy new file mode 100644 index 0000000..137eeea --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryModeSpec.groovy @@ -0,0 +1,331 @@ +/* + * 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.toolkit.encryptconfig + +import org.apache.nifi.toolkit.encryptconfig.util.BootstrapUtil +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Specification + +import java.security.Security + +import static org.apache.nifi.toolkit.encryptconfig.TestUtil.* + +class NiFiRegistryModeSpec extends Specification { + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryModeSpec.class) + + // runs before every feature method + def setup() {} + + // runs after every feature method + def cleanup() {} + + // runs before the first feature method + def setupSpec() { + Security.addProvider(new BouncyCastleProvider()) + setupTmpDir() + } + + // runs after the last feature method + def cleanupSpec() { + cleanupTmpDir() + } + + def "writing key to bootstrap.conf file"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inBootstrapConf2 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inBootstrapConf3 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def outBootstrapConf3 = generateTmpFilePath() + def inBootstrapConf4 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def outBootstrapConf4 = generateTmpFilePath() + def inBootstrapConf5 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_KEY_128) + def outBootstrapConf5 = generateTmpFilePath() + + when: "run with args: -k <key> -b <file>" + tool.run("-k ${KEY_HEX_128} -b ${inBootstrapConf1}".split(" ")) + then: "key is written to input bootstrap.conf" + assertBootstrapFilesAreEqual(RESOURCE_REGISTRY_BOOTSTRAP_KEY_128, inBootstrapConf1, true) + + when: "run with args: -p <password> -b <file>" + tool.run("-p ${PASSWORD} -b ${inBootstrapConf2}".split(" ")) + then: "key derived from password is written to input bootstrap.conf" + PASSWORD_KEY_HEX == readKeyFromBootstrap(inBootstrapConf2) + + when: "run with args: -k <key> -b <file> -B <file>" + tool.run("-k ${KEY_HEX_128} -b ${inBootstrapConf3} -B ${outBootstrapConf3}".split(" ")) + then: "key is written to output bootstrap.conf" + assertBootstrapFilesAreEqual(RESOURCE_REGISTRY_BOOTSTRAP_KEY_128, outBootstrapConf3, true) + and: "input bootstrap.conf is unchanged" + assertBootstrapFilesAreEqual(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT, inBootstrapConf3, true) + + when: "run with args: -p <key> -b <file> -B <file>" + tool.run("-p ${PASSWORD} -b ${inBootstrapConf4} -B ${outBootstrapConf4}".split(" ")) + then: "key derived from password is written to output bootstrap.conf" + PASSWORD_KEY_HEX == readKeyFromBootstrap(outBootstrapConf4) + and: "input bootstrap.conf is unchanged" + assertBootstrapFilesAreEqual(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT, inBootstrapConf4, true) + + when: "run with args: -b <file> -B <file>" + tool.run("-b ${inBootstrapConf5} -B ${outBootstrapConf5}".split(" ")) + then: "key from input file is copied to output file" + KEY_HEX_128 == readKeyFromBootstrap(outBootstrapConf5) + assertBootstrapFilesAreEqual(inBootstrapConf5, outBootstrapConf5, true) + and: "input bootstrap.conf is unchanged" + assertBootstrapFilesAreEqual(RESOURCE_REGISTRY_BOOTSTRAP_KEY_128, inBootstrapConf5, true) + + } + + def "encrypt unprotected nifi-registry.properties file"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inRegistryProperties1 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED) + def inBootstrapConf2 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inRegistryProperties2 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED) + def outRegistryProperties2 = generateTmpFilePath() + def inBootstrapConf3 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inRegistryProperties3 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED) + def inRegistryProperties4 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED) + + when: "run with args: -k <key> -b <file> -r <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf1} -r ${inRegistryProperties1}".split(" ")) + then: "properties file is protected in place" + assertNiFiRegistryUnprotectedPropertiesAreProtected(inRegistryProperties1) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf1) + + when: "run with args: -k <key> -b <file> -r <file> -R <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf2} -r ${inRegistryProperties2} -R ${outRegistryProperties2}".split(" ")) + then: "output properties file is protected" + assertNiFiRegistryUnprotectedPropertiesAreProtected(outRegistryProperties2) + and: "input properties file is unchanged" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED, inRegistryProperties2, true) + and: "key is written to output bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf2) + + when: "run with args: -p <password> -b <file> -r <file>" + tool.run("-p ${PASSWORD} -b ${inBootstrapConf3} -r ${inRegistryProperties3}".split(" ")) + then: "properties file is protected in place" + assertNiFiRegistryUnprotectedPropertiesAreProtected(inRegistryProperties3) + and: "key is written to input bootstrap.conf" + PASSWORD_KEY_HEX == readKeyFromBootstrap(inBootstrapConf3) + + when: "run with args: -b <file_with_key> -r <file>" + tool.run("-b ${RESOURCE_REGISTRY_BOOTSTRAP_KEY_128} -r ${inRegistryProperties4}".split(" ")) + then: "properties file is protected in place using key from bootstrap" + assertNiFiRegistryUnprotectedPropertiesAreProtected(inRegistryProperties4, PROTECTION_SCHEME_128) + + } + + def "encrypt nifi-registry.properties with no sensitive properties is a no-op"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inRegistryProperties1 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_COMMENTED) + def inBootstrapConf2 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inRegistryProperties2 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_EMPTY) + def outRegistryProperties2 = generateTmpFilePath() + + when: "run with args: -k <key> -b <file> -r <file_with_no_sensitive_props>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf1} -r ${inRegistryProperties1}".split(" ")) + then: "properties file is unchanged" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_COMMENTED, inRegistryProperties1, true) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf1) + + when: "run with args: -k <key> -b <file> -r <file_with_empty_sensitive_props> -R <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf2} -r ${inRegistryProperties2} -R ${outRegistryProperties2}".split(" ")) + then: "input properties file is unchanged and output properties file matches input" + assertPropertiesFilesAreEqual(RESOURCE_REGISTRY_PROPERTIES_EMPTY, inRegistryProperties2, true) + assertPropertiesFilesAreEqual(inRegistryProperties2, outRegistryProperties2, true) + and: "key is written to output bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf2) + + } + + def "encrypt unprotected authorizers.xml file"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inAuthorizers1 = copyFileToTempFile(RESOURCE_REGISTRY_AUTHORIZERS_POPULATED_UNPROTECTED) + def inBootstrapConf2 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inAuthorizers2 = copyFileToTempFile(RESOURCE_REGISTRY_AUTHORIZERS_POPULATED_UNPROTECTED) + def outAuthorizers2 = generateTmpFilePath() + + when: "run with args: -k <key> -b <file> -a <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf1} -a ${inAuthorizers1}".split(" ")) + then: "authorizers file is protected in place" + assertRegistryAuthorizersXmlIsProtected(inAuthorizers1) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf1) + + when: "run with args: -k <key> -b <file> -a <file> -A <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf2} -a ${inAuthorizers2} -A ${outAuthorizers2}".split(" ")) + then: "authorizers file is protected in place" + assertRegistryAuthorizersXmlIsProtected(outAuthorizers2) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf2) + + } + + def "encrypt authorizers.xml with no sensitive properties is a no-op"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inAuthorizersXml1 = copyFileToTempFile(RESOURCE_REGISTRY_AUTHORIZERS_COMMENTED) + def inBootstrapConf2 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inAuthorizersXml2 = copyFileToTempFile(RESOURCE_REGISTRY_AUTHORIZERS_EMPTY) + def outAuthorizers2 = generateTmpFilePath() + + when: "run with args: -k <key> -b <file> -a <file_with_no_sensitive_props>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf1} -a ${inAuthorizersXml1}".split(" ")) + then: "authorizers file is unchanged" + assertFilesAreEqual(RESOURCE_REGISTRY_AUTHORIZERS_COMMENTED, inAuthorizersXml1) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf1) + + when: "run with args: -k <key> -b <file> -a <file_with_empty_sensitive_props> -A <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf2} -a ${inAuthorizersXml2} -A ${outAuthorizers2}".split(" ")) + then: "input authorizers file is unchanged and output authorizers matches input" + assertFilesAreEqual(RESOURCE_REGISTRY_AUTHORIZERS_EMPTY, inAuthorizersXml2) + assertFilesAreEqual(inAuthorizersXml2, outAuthorizers2) + and: "key is written to output bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf2) + + } + + def "encrypt unprotected identity-providers.xml file"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inIdentityProviders1 = copyFileToTempFile(RESOURCE_REGISTRY_IDENTITY_PROVIDERS_POPULATED_UNPROTECTED) + def inBootstrapConf2 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inIdentityProviders2 = copyFileToTempFile(RESOURCE_REGISTRY_IDENTITY_PROVIDERS_POPULATED_UNPROTECTED) + def outIdentityProviders2 = generateTmpFilePath() + + when: "run with args: -k <key> -b <file> -i <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf1} -i ${inIdentityProviders1}".split(" ")) + then: "identity providers file is protected in place" + assertRegistryIdentityProvidersXmlIsProtected(inIdentityProviders1) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf1) + + when: "run with args: -k <key> -b <file> -i <file> -I <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf2} -i ${inIdentityProviders2} -I ${outIdentityProviders2}".split(" ")) + then: "identity providers file is protected in place" + assertRegistryIdentityProvidersXmlIsProtected(outIdentityProviders2) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf2) + + } + + def "encrypt identity-providers.xml with no sensitive properties is a no-op"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inIdentityProviders1 = copyFileToTempFile(RESOURCE_REGISTRY_IDENTITY_PROVIDERS_COMMENTED) + def inBootstrapConf2 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inIdentityProviders2 = copyFileToTempFile(RESOURCE_REGISTRY_IDENTITY_PROVIDERS_EMPTY) + def outIdentityProviders2 = generateTmpFilePath() + + when: "run with args: -k <key> -b <file> -i <file_with_no_sensitive_props>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf1} -i ${inIdentityProviders1}".split(" ")) + then: "identity providers file is unchanged" + assertFilesAreEqual(RESOURCE_REGISTRY_IDENTITY_PROVIDERS_COMMENTED, inIdentityProviders1) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf1) + + when: "run with args: -k <key> -b <file> -i <file_with_empty_sensitive_props> -I <file>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf2} -i ${inIdentityProviders2} -I ${outIdentityProviders2}".split(" ")) + then: "identity providers file is unchanged and output identity providers matches input" + assertFilesAreEqual(RESOURCE_REGISTRY_IDENTITY_PROVIDERS_EMPTY, inIdentityProviders2) + assertFilesAreEqual(inIdentityProviders2, outIdentityProviders2) + and: "key is written to output bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf2) + + } + + def "encrypt full configuration with properties, authorizers, and identity providers"() { + + setup: + NiFiRegistryMode tool = new NiFiRegistryMode() + def inBootstrapConf1 = copyFileToTempFile(RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT) + def inRegistryProperties1 = copyFileToTempFile(RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED) + def inAuthorizers1 = copyFileToTempFile(RESOURCE_REGISTRY_AUTHORIZERS_POPULATED_UNPROTECTED) + def inIdentityProviders1 = copyFileToTempFile(RESOURCE_REGISTRY_IDENTITY_PROVIDERS_POPULATED_UNPROTECTED) + + when: "run with args: -k <key> -b <file> -r <file> -a <file_with_no_sensitive_props> -i <file_with_no_sensitive_props>" + tool.run("-k ${KEY_HEX} -b ${inBootstrapConf1} -r ${inRegistryProperties1} -a ${inAuthorizers1} -i ${inIdentityProviders1}".split(" ")) + then: "all files are protected" + assertNiFiRegistryUnprotectedPropertiesAreProtected(inRegistryProperties1) + assertRegistryAuthorizersXmlIsProtected(inAuthorizers1) + assertRegistryIdentityProvidersXmlIsProtected(inIdentityProviders1) + and: "key is written to input bootstrap.conf" + KEY_HEX == readKeyFromBootstrap(inBootstrapConf1) + + } + + //-- Helper Methods + + private static String readKeyFromBootstrap(String bootstrapPath) { + return BootstrapUtil.extractKeyFromBootstrapFile(bootstrapPath, BootstrapUtil.REGISTRY_BOOTSTRAP_KEY_PROPERTY) + } + + private static boolean assertNiFiRegistryUnprotectedPropertiesAreProtected( + String pathToProtectedProperties, + String expectedProtectionScheme = PROTECTION_SCHEME) { + return assertPropertiesAreProtected( + RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED, + pathToProtectedProperties, + RESOURCE_REGISTRY_PROPERTIES_SENSITIVE_PROPS, + expectedProtectionScheme) + } + + static boolean assertRegistryAuthorizersXmlIsProtected( + String pathToProtectedXmlToVerify, + String expectedProtectionScheme = PROTECTION_SCHEME, + String expectedKey = KEY_HEX) { + return assertRegistryAuthorizersXmlIsProtected( + RESOURCE_REGISTRY_AUTHORIZERS_POPULATED_UNPROTECTED, + pathToProtectedXmlToVerify, + expectedProtectionScheme, + expectedKey) + } + + static boolean assertRegistryIdentityProvidersXmlIsProtected( + String pathToProtectedXmlToVerify, + String expectedProtectionScheme = PROTECTION_SCHEME, + String expectedKey = KEY_HEX) { + return assertRegistryIdentityProvidersXmlIsProtected( + RESOURCE_REGISTRY_IDENTITY_PROVIDERS_POPULATED_UNPROTECTED, + pathToProtectedXmlToVerify, + expectedProtectionScheme, + expectedKey) + } + + +}
