From 547847e83dea90b17dc16573c1967782f282ef33 Mon Sep 17 00:00:00 2001 From: Andy LoPresto Date: Mon, 7 Nov 2016 20:36:59 -0800 Subject: [PATCH] NIFI-2654 Enabled encryption coverage for login-identity-providers.xml. Squashed commits: [5dd22a9] NIFI-2654 Updated administration guide with login-identity-providers.xml flags. Exposed master key retrieval code in NiFiPropertiesLoader. Added logic to decrypt login identity providers XML configuration. Updated login-identity-providers.xsd to include encryption scheme attribute. Added unit tests. (+18 squashed commits) Squashed commits: [57c815f] NIFI-2654 Resolved issue where empty LIP property elements could not be encrypted. Added unit test and resource. [27d7309] NIFI-2654 Wired in serialization logic to write logic for LIP. Added comprehensive unit test for LIP & NFP in same test. [b450eb2] NIFI-2654 Finalized logic for preserving comments in LIP parsing. [5aa6c9c] NIFI-2654 Added logic for maintaining XML formatting (comments and whitespace) for LIP. Added unit tests (w/o encryption works; w/ does not). [b53461f] NIFI-2654 Added unit test for full tool invocation migrating a login-identity-providers.xml file and updating file and bootstrap.conf with key. [2d9686c] NIFI-2654 Updated tool description and various logging statements. Added unit test for full tool invocation encrypting a login-identity-providers.xml file and updating file and bootstrap.conf with key. [8c67cb2] NIFI-2654 Added logic to encrypt LIP XML content. Added unit tests. [8682d19] NIFI-2654 Added logic to handle "empty" (commented) LIP files. Added unit tests. [077230e] NIFI-2654 Fixed logic to decrypt multiline and multiple-per-line XML elements. Added unit tests and resources. [d5bb8da] NIFI-2654 Ignored unit test for unreadable conf directory because directory was causing Maven build issues. Removed test resources. [7e50506] NIFI-2654 Fixed AESSensitivePropertyProvider bug handling cipher text with whitespace. Added unit test. [b69a661] NIFI-2654 Fixed AESSensitivePropertyProviderFactoryTest to reflect absence of key causes errors. [6f821b9] NIFI-2654 Added standard password to arbitrary encryption test for use in test resources. [d289ffa] NIFI-2654 Added LIP XML decryption. Added unit tests. [a482245] NIFI-2654 Added LIP test resources. [7204df4] NIFI-2654 Changed logic to only perform properties encryption when file path is provided. [729e1df] NIFI-2654 Removed population of default file locations for bootstrap.conf, nifi.properties, and login-identity-providers.xml as not all files may be desired. Added/updated unit tests. [7dba5ef] NIFI-2654 Started LIP work (arguments & parsing). Added unit tests. --- .../main/asciidoc/administration-guide.adoc | 41 +- .../AESSensitivePropertyProvider.java | 2 + .../nifi/properties/NiFiPropertiesLoader.java | 8 +- ...ensitivePropertyProviderFactoryTest.groovy | 20 +- .../AESSensitivePropertyProviderTest.groovy | 30 + .../NiFiPropertiesLoaderGroovyTest.groovy | 2 + .../unreadable_conf/nifi.properties | 14 - .../LoginIdentityProviderFactoryBean.java | 69 +- .../src/main/xsd/login-identity-providers.xsd | 3 +- ...oginIdentityProviderFactoryBeanTest.groovy | 141 ++++ .../properties/ConfigEncryptionTool.groovy | 253 +++++- .../ConfigEncryptionToolTest.groovy | 766 +++++++++++++++++- .../bootstrap_with_master_key_128.conf | 2 +- ...identity-providers-commented-populated.xml | 107 +++ .../login-identity-providers-commented.xml | 107 +++ ...gin-identity-providers-populated-empty.xml | 105 +++ ...roviders-populated-encrypted-multiline.xml | 110 +++ ...-populated-encrypted-multiple-per-line.xml | 101 +++ ...identity-providers-populated-encrypted.xml | 105 +++ ...identity-providers-populated-multiline.xml | 111 +++ ...-providers-populated-multiple-per-line.xml | 101 +++ ...entity-providers-populated-single-line.xml | 101 +++ .../login-identity-providers-populated.xml | 105 +++ .../resources/login-identity-providers.xml | 105 +++ 24 files changed, 2418 insertions(+), 91 deletions(-) delete mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/nifi.properties create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy rename nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/bootstrap.conf => nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_master_key_128.conf (95%) mode change 100755 => 100644 create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented-populated.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-empty.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiline.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiple-per-line.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiline.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiple-per-line.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-single-line.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated.xml create mode 100644 nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers.xml diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 2ce656429a94..be2e684742b5 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -987,17 +987,19 @@ The default encryption algorithm utilized is AES/GCM 128/256-bit. 128-bit is use You can use the following command line options with the `encrypt-config` tool: -* `-b,--bootstrapConf ` The bootstrap.conf file to persist master key -* `-e,--oldKey ` The old raw hexadecimal key to use during key migration -* `-h,--help` Prints this usage message -* `-k,--key ` The raw hexadecimal key to use to encrypt the sensitive properties -* `-m,--migrate` If provided, the sensitive properties will be re-encrypted with a new key -* `-n,--niFiProperties ` The nifi.properties file containing unprotected config values (will be overwritten) -* `-o,--outputNiFiProperties ` The destination nifi.properties file containing protected config values (will not modify input nifi.properties) -* `-p,--password ` The password from which to derive the key to use to encrypt the sensitive properties -* `-r,--useRawKey` If provided, the secure console will prompt for the raw key value in hexadecimal form -* `-v,--verbose` Sets verbose mode (default false) -* `-w,--oldPassword ` The old password from which to derive the key during migration +* `-b,--bootstrapConf ` The bootstrap.conf file to persist master key +* `-e,--oldKey ` The old raw hexadecimal key to use during key migration +* `-h,--help` Prints this usage message +* `-k,--key ` The raw hexadecimal key to use to encrypt the sensitive properties +* `-m,--migrate` If provided, the sensitive properties will be re-encrypted with a new key +* `-n,--niFiProperties ` The nifi.properties file containing unprotected config values (will be overwritten) +* `-o,--outputNiFiProperties ` The destination nifi.properties file containing protected config values (will not modify input nifi.properties) +* `-p,--password ` The password from which to derive the key to use to encrypt the sensitive properties +* `-r,--useRawKey` If provided, the secure console will prompt for the raw key value in hexadecimal form +* `-v,--verbose` Sets verbose mode (default false) +* `-w,--oldPassword ` The old password from which to derive the key during migration +* `-l,--loginIdentityProviders ` The login-identity-providers.xml file containing unprotected config values (will be overwritten) +* `-i,--outputLoginIdentityProviders ` The destination login-identity-providers.xml file containing protected config values (will not modify input login-identity-providers.xml) As an example of how the tool works, assume that you have installed the tool on a machine supporting 256-bit encryption and with the following existing values in the 'nifi.properties' file: @@ -1058,6 +1060,23 @@ Sensitive configuration values are encrypted by the tool by default, however you If the 'nifi.properties' file already has valid protected values, those property values are not modified by the tool. +When applied to 'login-identity-providers.xml', the property elements are updated with an `encryption` attribute: + +---- + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + someuser + q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA + + Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g + + ... + +---- + In order to change the key used to encrypt the sensitive values, indicate *migration mode* using the `-m` or `--migrate` flag, provide the new key or password using the `-k` or `-p` flags as usual, and provide the existing key or password using `-e` or `-w` respectively. This will allow the toolkit to decrypt the existing values and re-encrypt them, and update `bootstrap.conf` with the new key. Only one of the key or password needs to be specified for each phase (old vs. new), and any combination is sufficient: * old key -> new key diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java index 770d55d62825..1df398ebda84 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java @@ -217,6 +217,8 @@ public String unprotect(String protectedValue) throws SensitivePropertyProtectio throw new IllegalArgumentException("The cipher text does not contain the delimiter " + DELIMITER + " -- it should be of the form Base64(IV) || Base64(cipherText)"); } + protectedValue = protectedValue.trim(); + final String IV_B64 = protectedValue.substring(0, protectedValue.indexOf(DELIMITER)); byte[] iv = Base64.decode(IV_B64); if (iv.length < IV_LENGTH) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java index 7f89b3dfc99d..20b519178bcb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java @@ -102,7 +102,13 @@ public static NiFiProperties loadDefaultWithKeyFromBootstrap() throws IOExceptio } } - private static String extractKeyFromBootstrapFile() throws IOException { + /** + * Returns the key (if any) used to encrypt sensitive properties, extracted from {@code $NIFI_HOME/conf/bootstrap.conf}. + * + * @return the key in hexadecimal format + * @throws IOException if the file is not readable + */ + public static String extractKeyFromBootstrapFile() throws IOException { // Guess at location of bootstrap.conf file from nifi.properties file String defaultNiFiPropertiesPath = getDefaultFilePath(); File propertiesFile = new File(defaultNiFiPropertiesPath); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy index e18ac93c7125..115fca6be3d1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy @@ -56,17 +56,18 @@ class AESSensitivePropertyProviderFactoryTest extends GroovyTestCase { @Ignore("This is resolved in PR 1216") @Test - public void testShouldGetProviderWithoutKey() throws Exception { + public void testShouldNotGetProviderWithoutKey() throws Exception { // Arrange SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory() // Act - SensitivePropertyProvider provider = factory.getProvider() + def msg = shouldFail(SensitivePropertyProtectionException) { + SensitivePropertyProvider provider = factory.getProvider() + } + logger.expected(msg) // Assert - assert provider instanceof AESSensitivePropertyProvider - assert !provider.@key - assert !provider.@cipher + assert msg == "The provider factory cannot generate providers without a key" } @Test @@ -90,11 +91,12 @@ class AESSensitivePropertyProviderFactoryTest extends GroovyTestCase { SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory("") // Act - SensitivePropertyProvider provider = factory.getProvider() + def msg = shouldFail(SensitivePropertyProtectionException) { + SensitivePropertyProvider provider = factory.getProvider() + } + logger.expected(msg) // Assert - assert provider instanceof AESSensitivePropertyProvider - assert !provider.@key - assert !provider.@cipher + assert msg == "The provider factory cannot generate providers without a key" } } \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy index 3ca35f23b340..4c5a34d8f6aa 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy @@ -238,6 +238,36 @@ class AESSensitivePropertyProviderTest extends GroovyTestCase { } } + @Test + public void testShouldUnprotectValueWithWhitespace() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + Map encryptionCiphers = KEY_SIZES.collectEntries { int keySize -> + byte[] iv = new byte[IV_LENGTH] + secureRandom.nextBytes(iv) + [(keySize): getCipher(true, keySize, iv)] + } + + Map CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry e -> + String iv = encoder.encodeToString(e.value.getIV()) + String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8))) + [(e.key): "${iv}||${cipherText}"] + } + CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") } + + // Act + Map plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + [(keySize): spp.unprotect("\t" + cipherText + "\n")] + } + plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") } + + // Assert + assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT } + } + @Test public void testShouldHandleUnprotectMalformedValue() throws Exception { // Arrange diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy index eb1f0811aabf..980f86f4e481 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy @@ -22,6 +22,7 @@ import org.junit.After import org.junit.AfterClass import org.junit.Before import org.junit.BeforeClass +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -349,6 +350,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase { Files.setPosixFilePermissions(unreadableFile.toPath(), originalPermissions) } + @Ignore("Unreadable conf directory breaks build") @Test public void testShouldNotExtractKeyFromUnreadableConfDir() throws Exception { // Arrange diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/nifi.properties deleted file mode 100644 index ae1e83eeb3d4..000000000000 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/nifi.properties +++ /dev/null @@ -1,14 +0,0 @@ -# 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. diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java index 26fae82d391d..ee978dbf339f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java @@ -16,6 +16,22 @@ */ package org.apache.nifi.web.security.spring; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authentication.AuthenticationResponse; import org.apache.nifi.authentication.LoginCredentials; @@ -31,6 +47,11 @@ import org.apache.nifi.authentication.generated.Provider; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; +import org.apache.nifi.properties.AESSensitivePropertyProviderFactory; +import org.apache.nifi.properties.NiFiPropertiesLoader; +import org.apache.nifi.properties.SensitivePropertyProtectionException; +import org.apache.nifi.properties.SensitivePropertyProvider; +import org.apache.nifi.properties.SensitivePropertyProviderFactory; import org.apache.nifi.util.NiFiProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,22 +59,6 @@ import org.springframework.beans.factory.FactoryBean; import org.xml.sax.SAXException; -import javax.xml.XMLConstants; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; -import javax.xml.transform.stream.StreamSource; -import javax.xml.validation.Schema; -import javax.xml.validation.SchemaFactory; -import java.io.File; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - /** * */ @@ -64,6 +69,9 @@ public class LoginIdentityProviderFactoryBean implements FactoryBean, Disposable private static final String JAXB_GENERATED_PATH = "org.apache.nifi.authentication.generated"; private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); + private static SensitivePropertyProviderFactory SENSITIVE_PROPERTY_PROVIDER_FACTORY; + private static SensitivePropertyProvider SENSITIVE_PROPERTY_PROVIDER; + /** * Load the JAXBContext. */ @@ -185,12 +193,39 @@ private LoginIdentityProviderConfigurationContext loadLoginIdentityProviderConfi final Map providerProperties = new HashMap<>(); for (final Property property : provider.getProperty()) { - providerProperties.put(property.getName(), property.getValue()); + if (!StringUtils.isBlank(property.getEncryption())) { + String decryptedValue = decryptValue(property.getValue(), property.getEncryption()); + providerProperties.put(property.getName(), decryptedValue); + } else { + providerProperties.put(property.getName(), property.getValue()); + } } return new StandardLoginIdentityProviderConfigurationContext(provider.getIdentifier(), providerProperties); } + private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException { + initializeSensitivePropertyProvider(encryptionScheme); + return SENSITIVE_PROPERTY_PROVIDER.unprotect(cipherText); + } + + private static void initializeSensitivePropertyProvider(String encryptionScheme) throws SensitivePropertyProtectionException { + if (SENSITIVE_PROPERTY_PROVIDER == null || !SENSITIVE_PROPERTY_PROVIDER.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) { + try { + String keyHex = getMasterKey(); + SENSITIVE_PROPERTY_PROVIDER_FACTORY = new AESSensitivePropertyProviderFactory(keyHex); + SENSITIVE_PROPERTY_PROVIDER = SENSITIVE_PROPERTY_PROVIDER_FACTORY.getProvider(); + } catch (IOException e) { + logger.error("Error extracting master key from bootstrap.conf for login identity provider decryption", e); + throw new SensitivePropertyProtectionException("Could not read master key from bootstrap.conf"); + } + } + } + + private static String getMasterKey() throws IOException { + return NiFiPropertiesLoader.extractKeyFromBootstrapFile(); + } + private void performMethodInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd index 628f390f0700..12a85ca3cb8c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd @@ -27,7 +27,8 @@ - + + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy new file mode 100644 index 000000000000..13eab19ed1f2 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy @@ -0,0 +1,141 @@ +/* + * 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.web.security.spring + +import org.apache.nifi.authentication.generated.Property +import org.apache.nifi.authentication.generated.Provider +import org.apache.nifi.properties.AESSensitivePropertyProvider +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Ignore +import org.junit.Test +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 LoginIdentityProviderFactoryBeanTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(LoginIdentityProviderFactoryBeanTest.class) + + // These blocks configure the constant values depending on JCE policies of the machine running the tests + private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210" + private static final String KEY_HEX_256 = KEY_HEX_128 * 2 + public static final String KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? KEY_HEX_256 : KEY_HEX_128 + + private static final String CIPHER_TEXT_128 = "6pqdM1urBEPHtj+L||ds0Z7RpqOA2321c/+7iPMfxDrqmH5Qx6UwQG0eIYB//3Ng" + private static final String CIPHER_TEXT_256 = "TepMCD7v3LAMF0KX||ydSRWPRl1/JXgTsZtfzCnDXu7a0lTLysjPL2I06EPUCHzw" + public static final String CIPHER_TEXT = isUnlimitedStrengthCryptoAvailable() ? CIPHER_TEXT_256 : CIPHER_TEXT_128 + + private static final String ENCRYPTION_SCHEME_128 = "aes/gcm/128" + private static final String ENCRYPTION_SCHEME_256 = "aes/gcm/256" + public static + final String ENCRYPTION_SCHEME = isUnlimitedStrengthCryptoAvailable() ? ENCRYPTION_SCHEME_256 : ENCRYPTION_SCHEME_128 + + private static final String PASSWORD = "thisIsABadPassword" + + @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(" ")}") + } + } + + @AfterClass + public static void tearDownOnce() throws Exception { + } + + @Before + public void setUp() throws Exception { + LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER = new AESSensitivePropertyProvider(KEY_HEX) + } + + @After + public void tearDown() throws Exception { + LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER = null + LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER_FACTORY = null + } + + private static boolean isUnlimitedStrengthCryptoAvailable() { + Cipher.getMaxAllowedKeyLength("AES") > 128 + } + + private static int getKeyLength(String keyHex = KEY_HEX) { + keyHex?.size() * 4 + } + + @Ignore("Can't test without overloading static metaClass method") + @Test + void testShouldInitializeSensitivePropertyProvider() { + // Arrange + assert !LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER + assert !LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER_FACTORY + + logger.info("Encryption scheme: ${ENCRYPTION_SCHEME}") + + // Act + LoginIdentityProviderFactoryBean.initializeSensitivePropertyProvider(ENCRYPTION_SCHEME) + + // Assert + assert LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER + assert LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER_FACTORY + assert LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER.getIdentifierKey() == ENCRYPTION_SCHEME + } + + @Test + void testShouldDecryptValue() { + // Arrange + logger.info("Encryption scheme: ${ENCRYPTION_SCHEME}") + logger.info("Cipher text: ${CIPHER_TEXT}") + + // Act + String decrypted = new LoginIdentityProviderFactoryBean().decryptValue(CIPHER_TEXT, ENCRYPTION_SCHEME) + logger.info("Decrypted ${CIPHER_TEXT} -> ${decrypted}") + + // Assert + assert decrypted == PASSWORD + } + + @Test + void testShouldLoadEncryptedLoginIdentityProviderConfiguration() { + // Arrange + Provider encryptedProvider = new Provider() + encryptedProvider.identifier ="ldap-provider" + encryptedProvider.clazz = "org.apache.nifi.ldap.LdapProvider" + def managerPasswordName = "Manager Password" + Property managerPasswordProperty = new Property(name: managerPasswordName, value: CIPHER_TEXT, encryption: ENCRYPTION_SCHEME) + encryptedProvider.property = [managerPasswordProperty] + + logger.info("Manager Password property: ${managerPasswordProperty.dump()}") + def bean = new LoginIdentityProviderFactoryBean() + + // Act + def context = bean.loadLoginIdentityProviderConfiguration(encryptedProvider) + logger.info("Loaded context: ${context.dump()}") + + // Assert + assert context.getProperty(managerPasswordName) == PASSWORD + } +} diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy index 30a682c4109a..dc11df46fca8 100644 --- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy @@ -17,6 +17,7 @@ package org.apache.nifi.properties import groovy.io.GroovyPrintWriter +import groovy.xml.XmlUtil import org.apache.commons.cli.CommandLine import org.apache.commons.cli.CommandLineParser import org.apache.commons.cli.DefaultParser @@ -46,23 +47,30 @@ class ConfigEncryptionTool { public String niFiPropertiesPath public String outputNiFiPropertiesPath public String loginIdentityProvidersPath + public String outputLoginIdentityProvidersPath private String keyHex private String migrationKeyHex private String password private String migrationPassword + private NiFiProperties niFiProperties + private String loginIdentityProviders private boolean usingPassword = true private boolean usingPasswordMigration = true private boolean migration = false private boolean isVerbose = false + private boolean handlingNiFiProperties = false + private boolean handlingLoginIdentityProviders = false private static final String HELP_ARG = "help" private static final String VERBOSE_ARG = "verbose" private static final String BOOTSTRAP_CONF_ARG = "bootstrapConf" private static final String NIFI_PROPERTIES_ARG = "niFiProperties" + private static final String LOGIN_IDENTITY_PROVIDERS_ARG = "loginIdentityProviders" private static final String OUTPUT_NIFI_PROPERTIES_ARG = "outputNiFiProperties" + private static final String OUTPUT_LOGIN_IDENTITY_PROVIDERS_ARG = "outputLoginIdentityProviders" private static final String KEY_ARG = "key" private static final String PASSWORD_ARG = "password" private static final String KEY_MIGRATION_ARG = "oldKey" @@ -87,7 +95,9 @@ class ConfigEncryptionTool { private static final String FOOTER = buildFooter() private static - final String DEFAULT_DESCRIPTION = "This tool reads from a nifi.properties file with plain sensitive configuration values, prompts the user for a master key, and encrypts each value. It will replace the plain value with the protected value in the same file (or write to a new nifi.properties file if specified)." + final String DEFAULT_DESCRIPTION = "This tool reads from a nifi.properties and/or login-identity-providers.xml file with plain sensitive configuration values, prompts the user for a master key, and encrypts each value. It will replace the plain value with the protected value in the same file (or write to a new file if specified)." + static private final String LDAP_PROVIDER_REGEX = /\s*\s*ldap-provider[\s\S]*?<\/provider>/ + static private final String XML_DECLARATION_REGEX = /<\?xml version="1.0" encoding="UTF-8"\?>/ private static String buildHeader(String description = DEFAULT_DESCRIPTION) { "${SEP}${description}${SEP * 2}" @@ -111,8 +121,10 @@ class ConfigEncryptionTool { options.addOption("h", HELP_ARG, false, "Prints this usage message") options.addOption("v", VERBOSE_ARG, false, "Sets verbose mode (default false)") options.addOption("n", NIFI_PROPERTIES_ARG, true, "The nifi.properties file containing unprotected config values (will be overwritten)") + options.addOption("l", LOGIN_IDENTITY_PROVIDERS_ARG, true, "The login-identity-providers.xml file containing unprotected config values (will be overwritten)") options.addOption("b", BOOTSTRAP_CONF_ARG, true, "The bootstrap.conf file to persist master key") options.addOption("o", OUTPUT_NIFI_PROPERTIES_ARG, true, "The destination nifi.properties file containing protected config values (will not modify input nifi.properties)") + options.addOption("i", OUTPUT_LOGIN_IDENTITY_PROVIDERS_ARG, true, "The destination login-identity-providers.xml file containing protected config values (will not modify input login-identity-providers.xml)") options.addOption("k", KEY_ARG, true, "The raw hexadecimal key to use to encrypt the sensitive properties") options.addOption("e", KEY_MIGRATION_ARG, true, "The old raw hexadecimal key to use during key migration") options.addOption("p", PASSWORD_ARG, true, "The password from which to derive the key to use to encrypt the sensitive properties") @@ -141,6 +153,7 @@ class ConfigEncryptionTool { throw new CommandLineParseException(errorMessage, exitCode); } + // TODO: Refactor component steps into methods protected CommandLine parse(String[] args) throws CommandLineParseException { CommandLineParser parser = new DefaultParser() CommandLine commandLine @@ -152,15 +165,49 @@ class ConfigEncryptionTool { isVerbose = commandLine.hasOption(VERBOSE_ARG) - bootstrapConfPath = commandLine.getOptionValue(BOOTSTRAP_CONF_ARG, determineDefaultBootstrapConfPath()) - niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG, determineDefaultNiFiPropertiesPath()) - outputNiFiPropertiesPath = commandLine.getOptionValue(OUTPUT_NIFI_PROPERTIES_ARG, niFiPropertiesPath) + bootstrapConfPath = commandLine.getOptionValue(BOOTSTRAP_CONF_ARG) + + if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) { + if (isVerbose) { + logger.info("Handling encryption of nifi.properties") + } + niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG) + outputNiFiPropertiesPath = commandLine.getOptionValue(OUTPUT_NIFI_PROPERTIES_ARG, niFiPropertiesPath) + handlingNiFiProperties = true - if (niFiPropertiesPath == outputNiFiPropertiesPath) { - // TODO: Add confirmation pause and provide -y flag to offer no-interaction mode? - logger.warn("The source nifi.properties and destination nifi.properties are identical [${outputNiFiPropertiesPath}] so the original will be overwritten") + if (niFiPropertiesPath == outputNiFiPropertiesPath) { + // TODO: Add confirmation pause and provide -y flag to offer no-interaction mode? + logger.warn("The source nifi.properties and destination nifi.properties are identical [${outputNiFiPropertiesPath}] so the original will be overwritten") + } } + if (commandLine.hasOption(LOGIN_IDENTITY_PROVIDERS_ARG)) { + if (isVerbose) { + logger.info("Handling encryption of login-identity-providers.xml") + } + loginIdentityProvidersPath = commandLine.getOptionValue(LOGIN_IDENTITY_PROVIDERS_ARG) + outputLoginIdentityProvidersPath = commandLine.getOptionValue(OUTPUT_LOGIN_IDENTITY_PROVIDERS_ARG, loginIdentityProvidersPath) + handlingLoginIdentityProviders = true + + if (loginIdentityProvidersPath == outputLoginIdentityProvidersPath) { + // TODO: Add confirmation pause and provide -y flag to offer no-interaction mode? + logger.warn("The source login-identity-providers.xml and destination login-identity-providers.xml are identical [${outputLoginIdentityProvidersPath}] so the original will be overwritten") + } + } + + if (isVerbose) { + logger.info(" bootstrap.conf: \t${bootstrapConfPath}") + logger.info("(src) nifi.properties: \t${niFiPropertiesPath}") + logger.info("(dest) nifi.properties: \t${outputNiFiPropertiesPath}") + logger.info("(src) login-identity-providers.xml: \t${loginIdentityProvidersPath}") + logger.info("(dest) login-identity-providers.xml: \t${outputLoginIdentityProvidersPath}") + } + + // TODO: Implement in NIFI-2655 +// if (!commandLine.hasOption(NIFI_PROPERTIES_ARG) && !commandLine.hasOption(LOGIN_IDENTITY_PROVIDERS_ARG)) { +// printUsageAndThrow("One of '-n'/'--${NIFI_PROPERTIES_ARG}' or '-l'/'--${LOGIN_IDENTITY_PROVIDERS_ARG}' must be provided", ExitCode.INVALID_ARGS) +// } + if (commandLine.hasOption(MIGRATION_ARG)) { migration = true if (isVerbose) { @@ -169,7 +216,7 @@ class ConfigEncryptionTool { if (commandLine.hasOption(PASSWORD_MIGRATION_ARG)) { usingPasswordMigration = true if (commandLine.hasOption(KEY_MIGRATION_ARG)) { - printUsageAndThrow("Only one of ${PASSWORD_MIGRATION_ARG} and ${KEY_MIGRATION_ARG} can be used", ExitCode.INVALID_ARGS) + printUsageAndThrow("Only one of '-w'/'--${PASSWORD_MIGRATION_ARG}' and '-e'/'--${KEY_MIGRATION_ARG}' can be used", ExitCode.INVALID_ARGS) } else { migrationPassword = commandLine.getOptionValue(PASSWORD_MIGRATION_ARG) } @@ -179,14 +226,14 @@ class ConfigEncryptionTool { } } else { if (commandLine.hasOption(PASSWORD_MIGRATION_ARG) || commandLine.hasOption(KEY_MIGRATION_ARG)) { - printUsageAndThrow("${PASSWORD_MIGRATION_ARG} and ${KEY_MIGRATION_ARG} are ignored unless ${MIGRATION_ARG} is enabled", ExitCode.INVALID_ARGS) + printUsageAndThrow("'-w'/'--${PASSWORD_MIGRATION_ARG}' and '-e'/'--${KEY_MIGRATION_ARG}' are ignored unless '-m'/'--${MIGRATION_ARG}' is enabled", ExitCode.INVALID_ARGS) } } if (commandLine.hasOption(PASSWORD_ARG)) { usingPassword = true if (commandLine.hasOption(KEY_ARG)) { - printUsageAndThrow("Only one of ${PASSWORD_ARG} and ${KEY_ARG} can be used", ExitCode.INVALID_ARGS) + printUsageAndThrow("Only one of '-p'/'--${PASSWORD_ARG}' and '-k'/'--${KEY_ARG}' can be used", ExitCode.INVALID_ARGS) } else { password = commandLine.getOptionValue(PASSWORD_ARG) } @@ -310,6 +357,111 @@ class ConfigEncryptionTool { } } + /** + * Loads the login identity providers configuration from the provided file path. + * + * @param existingKeyHex the key used to encrypt the configs (defaults to the current key) + * + * @return the file content + * @throw IOException if the login-identity-providers.xml file cannot be read + */ + private String loadLoginIdentityProviders(String existingKeyHex = keyHex) throws IOException { + File loginIdentityProvidersFile + if (loginIdentityProvidersPath && (loginIdentityProvidersFile = new File(loginIdentityProvidersPath)).exists()) { + try { + String xmlContent = loginIdentityProvidersFile.text + List lines = loginIdentityProvidersFile.readLines() + logger.info("Loaded LoginIdentityProviders content (${lines.size()} lines)") + String decryptedXmlContent = decryptLoginIdentityProviders(xmlContent, existingKeyHex) +// String decryptedXmlContent = ConfigEncryptionUtility.decryptLoginIdentityProviders(xmlContent) + return decryptedXmlContent + } catch (RuntimeException e) { + if (isVerbose) { + logger.error("Encountered an error", e) + } + throw new IOException("Cannot load LoginIdentityProviders from [${loginIdentityProvidersPath}]", e) + } + } else { + printUsageAndThrow("Cannot load LoginIdentityProviders from [${loginIdentityProvidersPath}]", ExitCode.ERROR_READING_NIFI_PROPERTIES) + } + } + + String decryptLoginIdentityProviders(String encryptedXml, String existingKeyHex = keyHex) { + AESSensitivePropertyProvider sensitivePropertyProvider = new AESSensitivePropertyProvider(existingKeyHex) + + try { + def doc = new XmlSlurper().parseText(encryptedXml) + def passwords = doc.provider.find { it.identifier == 'ldap-provider' }.property.findAll { + it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}" + } + + if (passwords.isEmpty()) { + if (isVerbose) { + logger.info("No encrypted password property elements found in login-identity-providers.xml") + } + return encryptedXml + } + + passwords.each { password -> + if (isVerbose) { + logger.info("Attempting to decrypt ${password.text()}") + } + String decryptedValue = sensitivePropertyProvider.unprotect(password.text().trim()) + password.replaceNode { + property(name: password.@name, encryption: "none", decryptedValue) + } + } + + // Does not preserve whitespace formatting or comments + String updatedXml = XmlUtil.serialize(doc) + logger.info("Updated XML content: ${updatedXml}") + updatedXml + } catch (Exception e) { + printUsageAndThrow("Cannot decrypt login identity providers XML content", ExitCode.SERVICE_ERROR) + } + } + + String encryptLoginIdentityProviders(String plainXml, String newKeyHex = keyHex) { + AESSensitivePropertyProvider sensitivePropertyProvider = new AESSensitivePropertyProvider(newKeyHex) + + // TODO: Switch to XmlParser & XmlNodePrinter to maintain "empty" element structure + try { + def doc = new XmlSlurper().parseText(plainXml) + // Only operate on un-encrypted passwords + def passwords = doc.provider.find { it.identifier == 'ldap-provider' } + .property.findAll { + it.@name =~ "Password" && (it.@encryption == "none" || it.@encryption == "") && it.text() + } + + if (passwords.isEmpty()) { + if (isVerbose) { + logger.info("No unencrypted password property elements found in login-identity-providers.xml") + } + return plainXml + } + + passwords.each { password -> + if (isVerbose) { + logger.info("Attempting to encrypt ${password.name()}") + } + String encryptedValue = sensitivePropertyProvider.protect(password.text().trim()) + password.replaceNode { + property(name: password.@name, encryption: sensitivePropertyProvider.identifierKey, encryptedValue) + } + } + + // Does not preserve whitespace formatting or comments + String updatedXml = XmlUtil.serialize(doc) + logger.info("Updated XML content: ${updatedXml}") + updatedXml + } catch (Exception e) { + if (isVerbose) { + logger.error("Encountered exception", e) + } + printUsageAndThrow("Cannot encrypt login identity providers XML content", ExitCode.SERVICE_ERROR) + } + } + /** * Accepts a {@link NiFiProperties} instance, iterates over all non-empty sensitive properties which are not already marked as protected, encrypts them using the master key, and updates the property with the protected value. Additionally, adds a new sibling property {@code x.y.z.protected=aes/gcm/{128,256}} for each indicating the encryption scheme used. * @@ -426,6 +578,39 @@ class ConfigEncryptionTool { lines } + /** + * Writes the contents of the login identity providers configuration file with encrypted values to the output {@code login-identity-providers.xml} file. + * + * @throw IOException if there is a problem reading or writing the login-identity-providers.xml file + */ + private void writeLoginIdentityProviders() throws IOException { + if (!outputLoginIdentityProvidersPath) { + throw new IllegalArgumentException("Cannot write encrypted properties to empty login-identity-providers.xml path") + } + + File outputLoginIdentityProvidersFile = new File(outputLoginIdentityProvidersPath) + + if (isSafeToWrite(outputLoginIdentityProvidersFile)) { + try { + String updatedXmlContent + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + if (loginIdentityProvidersFile.exists() && loginIdentityProvidersFile.canRead()) { + // Instead of just writing the XML content to a file, this method attempts to maintain the structure of the original file and preserves comments + updatedXmlContent = serializeLoginIdentityProvidersAndPreserveFormat(loginIdentityProviders, loginIdentityProvidersFile).join("\n") + } + + // Write the updated values back to the file + outputLoginIdentityProvidersFile.text = updatedXmlContent + } catch (IOException e) { + def msg = "Encountered an exception updating the login-identity-providers.xml file with the encrypted values" + logger.error(msg, e) + throw e + } + } else { + throw new IOException("The login-identity-providers.xml file at ${outputLoginIdentityProvidersPath} must be writable by the user running this tool") + } + } + /** * Writes the contents of the {@link NiFiProperties} instance with encrypted values to the output {@code nifi.properties} file. * @@ -502,6 +687,21 @@ class ConfigEncryptionTool { out.toString().split("\n") } + + private + static List serializeLoginIdentityProvidersAndPreserveFormat(String xmlContent, File originalLoginIdentityProvidersFile) { + def parsedXml = new XmlSlurper().parseText(xmlContent) + def provider = parsedXml.provider.find { it.identifier == "ldap-provider" } + def serializedProvider = new XmlUtil().serialize(provider) + // Remove XML declaration from top + serializedProvider = serializedProvider.replaceFirst(XML_DECLARATION_REGEX, "") + + // Find the provider element of the new XML in the file contents + String fileContents = originalLoginIdentityProvidersFile.text + fileContents = fileContents.replaceFirst(LDAP_PROVIDER_REGEX, serializedProvider) + fileContents.split("\n") + } + /** * Helper method which returns true if it is "safe" to write to the provided file. * @@ -527,6 +727,11 @@ class ConfigEncryptionTool { "${niFiToolkitPath ? niFiToolkitPath + "/" : ""}conf/nifi.properties" } + private static String determineDefaultLoginIdentityProvidersPath() { + String niFiToolkitPath = System.getenv(NIFI_TOOLKIT_HOME) ?: "" + "${niFiToolkitPath ? niFiToolkitPath + "/" : ""}conf/login-identity-providers.xml" + } + private static String deriveKeyFromPassword(String password) { password = password?.trim() if (!password || password.length() < MIN_PASSWORD_LENGTH) { @@ -600,12 +805,23 @@ class ConfigEncryptionTool { } String existingKeyHex = tool.migrationKeyHex ?: tool.keyHex - try { - tool.niFiProperties = tool.loadNiFiProperties(existingKeyHex) - } catch (Exception e) { - tool.printUsageAndThrow("Cannot migrate key if no previous encryption occurred", ExitCode.ERROR_READING_NIFI_PROPERTIES) + if (tool.handlingNiFiProperties) { + try { + tool.niFiProperties = tool.loadNiFiProperties(existingKeyHex) + } catch (Exception e) { + tool.printUsageAndThrow("Cannot migrate key if no previous encryption occurred", ExitCode.ERROR_READING_NIFI_PROPERTIES) + } + tool.niFiProperties = tool.encryptSensitiveProperties(tool.niFiProperties) + } + + if (tool.handlingLoginIdentityProviders) { + try { + tool.loginIdentityProviders = tool.loadLoginIdentityProviders(existingKeyHex) + } catch (Exception e) { + tool.printUsageAndThrow("Cannot migrate key if no previous encryption occurred", ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS) + } + tool.loginIdentityProviders = tool.encryptLoginIdentityProviders(tool.loginIdentityProviders) } - tool.niFiProperties = tool.encryptSensitiveProperties(tool.niFiProperties) } catch (CommandLineParseException e) { if (e.exitCode == ExitCode.HELP) { System.exit(ExitCode.HELP.ordinal()) @@ -622,7 +838,12 @@ class ConfigEncryptionTool { // Do this as part of a transaction? synchronized (this) { tool.writeKeyToBootstrapConf() - tool.writeNiFiProperties() + if (tool.handlingNiFiProperties) { + tool.writeNiFiProperties() + } + if (tool.handlingLoginIdentityProviders) { + tool.writeLoginIdentityProviders() + } } } catch (Exception e) { if (tool.isVerbose) { diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy index c1521d7de9dd..17847483fce1 100644 --- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy @@ -25,6 +25,7 @@ import org.apache.nifi.util.console.TextDevice import org.apache.nifi.util.console.TextDevices import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.After +import org.junit.AfterClass import org.junit.Assume import org.junit.Before import org.junit.BeforeClass @@ -60,10 +61,15 @@ class ConfigEncryptionToolTest extends GroovyTestCase { public static final String KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? KEY_HEX_256 : KEY_HEX_128 private static final String PASSWORD = "thisIsABadPassword" // From ConfigEncryptionTool.deriveKeyFromPassword("thisIsABadPassword") - private static final String PASSWORD_KEY_HEX_256 = "2C576A9585DB862F5ECBEE5B4FFFCCA14B18D8365968D7081651006507AD2BDE" + private static + final String PASSWORD_KEY_HEX_256 = "2C576A9585DB862F5ECBEE5B4FFFCCA14B18D8365968D7081651006507AD2BDE" private static final String PASSWORD_KEY_HEX_128 = "2C576A9585DB862F5ECBEE5B4FFFCCA1" - private static final String PASSWORD_KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? PASSWORD_KEY_HEX_256 : PASSWORD_KEY_HEX_128 + private static + final String PASSWORD_KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? PASSWORD_KEY_HEX_256 : PASSWORD_KEY_HEX_128 + + private static final int LIP_PASSWORD_LINE_COUNT = 3 + private final String PASSWORD_PROP_REGEX = "]* name=\".* Password\"" @BeforeClass public static void setUpOnce() throws Exception { @@ -72,6 +78,14 @@ class ConfigEncryptionToolTest extends GroovyTestCase { logger.metaClass.methodMissing = { String name, args -> logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") } + + setupTmpDir() + } + + @AfterClass + public static void tearDownOnce() throws Exception { + File tmpDir = new File("target/tmp/") + tmpDir.delete() } @Before @@ -87,6 +101,10 @@ class ConfigEncryptionToolTest extends GroovyTestCase { Cipher.getMaxAllowedKeyLength("AES") > 128 } + private static int getKeyLength(String keyHex = KEY_HEX) { + keyHex?.size() * 4 + } + private static void printProperties(NiFiProperties properties) { if (!(properties instanceof ProtectedNiFiProperties)) { properties = new ProtectedNiFiProperties(properties) @@ -139,6 +157,13 @@ class ConfigEncryptionToolTest extends GroovyTestCase { formattedDate =~ datePattern } + private static File setupTmpDir(String tmpDirPath = "target/tmp/") { + File tmpDir = new File(tmpDirPath) + tmpDir.mkdirs() + setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE]) + tmpDir + } + @Test void testShouldPrintHelpMessage() { // Arrange @@ -174,20 +199,6 @@ class ConfigEncryptionToolTest extends GroovyTestCase { } } - @Test - void testParseShouldPopulateDefaultBootstrapConfArgument() { - // Arrange - String bootstrapPath = "conf/bootstrap.conf" - ConfigEncryptionTool tool = new ConfigEncryptionTool() - - // Act - tool.parse([] as String[]) - logger.info("Parsed bootstrap.conf location: ${tool.bootstrapConfPath}") - - // Assert - assert new File(tool.bootstrapConfPath).getPath() == new File(bootstrapPath).getPath() - } - @Test void testShouldParseNiFiPropertiesArgument() { // Arrange @@ -202,9 +213,12 @@ class ConfigEncryptionToolTest extends GroovyTestCase { // Assert assert tool.niFiPropertiesPath == niFiPropertiesPath + assert tool.handlingNiFiProperties } } + // TODO: Remove as part of NIFI-2655 + @Ignore("Remove as part of NIFI-2655") @Test void testParseShouldPopulateDefaultNiFiPropertiesArgument() { // Arrange @@ -228,7 +242,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase { // Act flags.each { String arg -> - tool.parse([arg, niFiPropertiesPath] as String[]) + tool.parse([arg, niFiPropertiesPath, "-n", niFiPropertiesPath] as String[]) logger.info("Parsed output nifi.properties location: ${tool.outputNiFiPropertiesPath}") // Assert @@ -236,6 +250,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase { } } + // TODO: Remove as part of NIFI-2655 + @Ignore("Remove as part of NIFI-2655") @Test void testParseShouldPopulateDefaultOutputNiFiPropertiesArgument() { // Arrange @@ -266,6 +282,91 @@ class ConfigEncryptionToolTest extends GroovyTestCase { assert TestAppender.events.first().message =~ "The source nifi.properties and destination nifi.properties are identical \\[.*\\] so the original will be overwritten" } + @Test + void testShouldParseLoginIdentityProvidersArgument() { + // Arrange + def flags = ["-l", "--loginIdentityProviders"] + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers.xml" + ConfigEncryptionTool tool = new ConfigEncryptionTool() + + // Act + flags.each { String arg -> + tool.parse([arg, loginIdentityProvidersPath] as String[]) + logger.info("Parsed login-identity-providers.xml location: ${tool.loginIdentityProvidersPath}") + + // Assert + assert tool.loginIdentityProvidersPath == loginIdentityProvidersPath + assert tool.handlingLoginIdentityProviders + } + } + + // TODO: Remove as part of NIFI-2655 + @Ignore("Remove as part of NIFI-2655") + @Test + void testParseShouldPopulateDefaultLoginIdentityProvidersArgument() { + // Arrange + String loginIdentityProvidersPath = "conf/login-identity-providers.xml" + ConfigEncryptionTool tool = new ConfigEncryptionTool() + + // Act + tool.parse([] as String[]) + logger.info("Parsed login-identity-providers.xml location: ${tool.loginIdentityProvidersPath}") + + // Assert + assert new File(tool.loginIdentityProvidersPath).getPath() == new File(loginIdentityProvidersPath).getPath() + } + + @Test + void testShouldParseOutputLoginIdentityProvidersArgument() { + // Arrange + def flags = ["-i", "--outputLoginIdentityProviders"] + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers.xml" + ConfigEncryptionTool tool = new ConfigEncryptionTool() + + // Act + flags.each { String arg -> + tool.parse([arg, loginIdentityProvidersPath, "-l", loginIdentityProvidersPath] as String[]) + logger.info("Parsed output login-identity-providers.xml location: ${tool.outputLoginIdentityProvidersPath}") + + // Assert + assert tool.outputLoginIdentityProvidersPath == loginIdentityProvidersPath + } + } + + // TODO: Remove as part of NIFI-2655 + @Ignore("Remove as part of NIFI-2655") + @Test + void testParseShouldPopulateDefaultOutputLoginIdentityProvidersArgument() { + // Arrange + String loginIdentityProvidersPath = "conf/login-identity-providers.xml" + ConfigEncryptionTool tool = new ConfigEncryptionTool() + + // Act + tool.parse([] as String[]) + logger.info("Parsed output login-identity-providers.xml location: ${tool.outputLoginIdentityProvidersPath}") + + // Assert + assert new File(tool.outputLoginIdentityProvidersPath).getPath() == new File(loginIdentityProvidersPath).getPath() + } + + @Test + void testParseShouldWarnIfLoginIdentityProvidersWillBeOverwritten() { + // Arrange + String loginIdentityProvidersPath = "conf/login-identity-providers.xml" + ConfigEncryptionTool tool = new ConfigEncryptionTool() + + // Act + tool.parse("-l ${loginIdentityProvidersPath} -i ${loginIdentityProvidersPath}".split(" ") as String[]) + logger.info("Parsed login-identity-providers.xml location: ${tool.loginIdentityProvidersPath}") + logger.info("Parsed output login-identity-providers.xml location: ${tool.outputLoginIdentityProvidersPath}") + + // Assert + assert !TestAppender.events.isEmpty() + assert TestAppender.events.any { + it.message =~ "The source login-identity-providers.xml and destination login-identity-providers.xml are identical \\[.*\\] so the original will be overwritten" + } + } + @Test void testShouldParseKeyArgument() { // Arrange @@ -294,7 +395,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase { logger.expected(msg) // Assert - assert msg =~ "Only one of oldPassword and oldKey can be used" + assert msg =~ "Only one of '-w'/'--oldPassword' and '-e'/'--oldKey' can be used" } @Test @@ -307,13 +408,14 @@ class ConfigEncryptionToolTest extends GroovyTestCase { // Act argStrings.each { String argString -> + argString += " -n any/path" def msg = shouldFail { tool.parse(argString.split(" ") as String[]) } logger.expected(msg) // Assert - assert msg == "oldPassword and oldKey are ignored unless migrate is enabled" + assert msg == "'-w'/'--oldPassword' and '-e'/'--oldKey' are ignored unless '-m'/'--migrate' is enabled" } } @@ -597,7 +699,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase { @Test void testShouldHandleKeyAndPasswordFlag() { // Arrange - def args = ["-k", KEY_HEX, "-p", PASSWORD] + def args = ["-k", KEY_HEX, "-p", PASSWORD, "-n", ""] logger.info("Using args: ${args}") // Act @@ -607,7 +709,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase { logger.expected(msg) // Assert - assert msg == "Only one of password and key can be used" + assert msg == "Only one of '-p'/'--password' and '-k'/'--key' can be used" } @Test @@ -1652,6 +1754,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase { exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { + assert outputPropertiesFile.exists() final List updatedPropertiesLines = outputPropertiesFile.readLines() logger.info("Updated nifi.properties:") logger.info("\n" * 2 + updatedPropertiesLines.join("\n")) @@ -1751,6 +1854,627 @@ class ConfigEncryptionToolTest extends GroovyTestCase { // Assertions in common method above } + + @Test + void testShouldDecryptLoginIdentityProviders() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated-encrypted.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + + // Sanity check for decryption + String cipherText = "q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA" + String EXPECTED_PASSWORD = "thisIsABadPassword" + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX_128) + assert spp.unprotect(cipherText) == EXPECTED_PASSWORD + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def decryptedLines = tool.decryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") + + // Assert + def passwordLines = decryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == LIP_PASSWORD_LINE_COUNT + assert passwordLines.every { it =~ ">thisIsABadPassword<" } + // Some lines were not encrypted originally so the encryption attribute would not have been updated + assert passwordLines.any { it =~ "encryption=\"none\"" } + } + + @Test + void testShouldDecryptLoginIdentityProvidersWithMultilineElements() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated-encrypted-multiline.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def decryptedLines = tool.decryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") + + // Assert + def passwordLines = decryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == LIP_PASSWORD_LINE_COUNT + assert passwordLines.every { it =~ ">thisIsABadPassword<" } + // Some lines were not encrypted originally so the encryption attribute would not have been updated + assert passwordLines.any { it =~ "encryption=\"none\"" } + } + + @Test + void testShouldDecryptLoginIdentityProvidersWithMultipleElementsPerLine() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated-encrypted-multiple-per-line.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def decryptedLines = tool.decryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") + + // Assert + def passwordLines = decryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == LIP_PASSWORD_LINE_COUNT + assert passwordLines.every { it =~ ">thisIsABadPassword<" } + // Some lines were not encrypted originally so the encryption attribute would not have been updated + assert passwordLines.any { it =~ "encryption=\"none\"" } + } + + + @Test + void testDecryptLoginIdentityProvidersShouldHandleCommentedElements() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-commented.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def decryptedLines = tool.decryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") + + // Assert + + // If no encrypted properties are found, the original input text is just returned (comments and formatting in tact) + assert decryptedLines == lines + } + + @Test + void testShouldEncryptLoginIdentityProviders() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == LIP_PASSWORD_LINE_COUNT + assert passwordLines.every { !it.contains(">thisIsABadPassword<") } + assert passwordLines.every { it.contains(encryptionScheme) } + passwordLines.each { + String ct = (it =~ ">(.*)")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testShouldEncryptLoginIdentityProvidersWithEmptySensitiveElements() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated-empty.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == LIP_PASSWORD_LINE_COUNT + def populatedPasswordLines = passwordLines.findAll { it.contains(">.*<") } + assert populatedPasswordLines.every { !it.contains(">thisIsABadPassword<") } + assert populatedPasswordLines.every { it.contains(encryptionScheme) } + populatedPasswordLines.each { + String ct = (it =~ ">(.*)")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testShouldEncryptLoginIdentityProvidersWithMultilineElements() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated-multiline.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == LIP_PASSWORD_LINE_COUNT + assert passwordLines.every { !it.contains(">thisIsABadPassword<") } + assert passwordLines.every { it.contains(encryptionScheme) } + passwordLines.each { + String ct = (it =~ ">(.*)")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testShouldEncryptLoginIdentityProvidersWithMultipleElementsPerLine() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated-multiple-per-line.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == LIP_PASSWORD_LINE_COUNT + assert passwordLines.every { !it.contains(">thisIsABadPassword<") } + assert passwordLines.every { it.contains(encryptionScheme) } + passwordLines.each { + String ct = (it =~ ">(.*)")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testEncryptLoginIdentityProvidersShouldHandleCommentedElements() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-commented.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def encryptedLines = tool.encryptLoginIdentityProviders(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + + // If no sensitive properties are found, the original input text is just returned (comments and formatting in tact) + assert encryptedLines == lines + } + + @Test + void testShouldPerformFullOperationForLoginIdentityProviders() { + // 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 originalBootstrapLines = bootstrapFile.readLines() + String originalKeyLine = originalBootstrapLines.find { + it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) + } + logger.info("Original key line from bootstrap.conf: ${originalKeyLine}") + assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + + final String EXPECTED_KEY_LINE = ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX + + File inputLIPFile = new File("src/test/resources/login-identity-providers-populated.xml") + File outputLIPFile = new File("target/tmp/tmp-lip.xml") + outputLIPFile.delete() + + String originalXmlContent = inputLIPFile.text + logger.info("Original XML content: ${originalXmlContent}") + + String[] args = ["-l", inputLIPFile.path, "-b", bootstrapFile.path, "-i", outputLIPFile.path, "-k", KEY_HEX, "-v"] + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + exit.checkAssertionAfterwards(new Assertion() { + public void checkAssertion() { + final String updatedXmlContent = outputLIPFile.text + logger.info("Updated XML content: ${updatedXmlContent}") + + // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) + def originalParsedXml = new XmlSlurper().parseText(originalXmlContent) + def updatedParsedXml = new XmlSlurper().parseText(updatedXmlContent) + assert originalParsedXml != updatedParsedXml + assert originalParsedXml.'**'.findAll { it.@encryption } != updatedParsedXml.'**'.findAll { + it.@encryption + } + + def encryptedValues = updatedParsedXml.provider.find { + it.identifier == 'ldap-provider' + }.property.findAll { + it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}" + } + + encryptedValues.each { + assert spp.unprotect(it.text()) == PASSWORD + } + + // Check that the key was persisted to the bootstrap.conf + final List updatedBootstrapLines = bootstrapFile.readLines() + String updatedKeyLine = updatedBootstrapLines.find { + it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) + } + logger.info("Updated key line: ${updatedKeyLine}") + + assert updatedKeyLine == EXPECTED_KEY_LINE + assert originalBootstrapLines.size() == updatedBootstrapLines.size() + + // Clean up + outputLIPFile.deleteOnExit() + bootstrapFile.deleteOnExit() + tmpDir.deleteOnExit() + } + }); + + // Act + ConfigEncryptionTool.main(args) + logger.info("Invoked #main with ${args.join(" ")}") + + // Assert + + // Assertions defined above + } + + @Test + void testShouldPerformFullOperationMigratingLoginIdentityProviders() { + // Arrange + exit.expectSystemExitWithStatus(0) + + File tmpDir = setupTmpDir() + + // Start with 128-bit encryption and go to whatever is supported on this system + File emptyKeyFile = new File("src/test/resources/bootstrap_with_master_key_128.conf") + File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf") + bootstrapFile.delete() + + Files.copy(emptyKeyFile.toPath(), bootstrapFile.toPath()) + final List originalBootstrapLines = bootstrapFile.readLines() + String originalKeyLine = originalBootstrapLines.find { + it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) + } + logger.info("Original key line from bootstrap.conf: ${originalKeyLine}") + assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX_128 + + final String EXPECTED_KEY_LINE = ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + PASSWORD_KEY_HEX + + File inputLIPFile = new File("src/test/resources/login-identity-providers-populated-encrypted.xml") + File outputLIPFile = new File("target/tmp/tmp-lip.xml") + outputLIPFile.delete() + + String originalXmlContent = inputLIPFile.text + logger.info("Original XML content: ${originalXmlContent}") + + // Migrate from KEY_HEX_128 to PASSWORD_KEY_HEX + String[] args = ["-l", inputLIPFile.path, "-b", bootstrapFile.path, "-i", outputLIPFile.path, "-m", "-e", KEY_HEX_128, "-k", PASSWORD_KEY_HEX, "-v"] + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(PASSWORD_KEY_HEX) + + exit.checkAssertionAfterwards(new Assertion() { + public void checkAssertion() { + final String updatedXmlContent = outputLIPFile.text + logger.info("Updated XML content: ${updatedXmlContent}") + + // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) + def originalParsedXml = new XmlSlurper().parseText(originalXmlContent) + def updatedParsedXml = new XmlSlurper().parseText(updatedXmlContent) + assert originalParsedXml != updatedParsedXml +// assert originalParsedXml.'**'.findAll { it.@encryption } != updatedParsedXml.'**'.findAll { it.@encryption } + + def encryptedValues = updatedParsedXml.provider.find { + it.identifier == 'ldap-provider' + }.property.findAll { + it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}" + } + + encryptedValues.each { + assert spp.unprotect(it.text()) == PASSWORD + } + + // Check that the key was persisted to the bootstrap.conf + final List updatedBootstrapLines = bootstrapFile.readLines() + String updatedKeyLine = updatedBootstrapLines.find { + it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) + } + logger.info("Updated key line: ${updatedKeyLine}") + + assert updatedKeyLine == EXPECTED_KEY_LINE + assert originalBootstrapLines.size() == updatedBootstrapLines.size() + + // Clean up + outputLIPFile.deleteOnExit() + bootstrapFile.deleteOnExit() + tmpDir.deleteOnExit() + } + }); + + // Act + ConfigEncryptionTool.main(args) + logger.info("Invoked #main with ${args.join(" ")}") + + // Assert + + // Assertions defined above + } + + @Test + void testSerializeLoginIdentityProvidersAndPreserveFormatShouldRespectComments() { + // Arrange + String loginIdentityProvidersPath = "src/test/resources/login-identity-providers-populated.xml" + File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) + + File tmpDir = setupTmpDir() + + File workingFile = new File("target/tmp/tmp-login-identity-providers.xml") + workingFile.delete() + Files.copy(loginIdentityProvidersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + // Just need to read the lines from the original file, parse them to XML, serialize back, and compare output, as no transformation operation will occur + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + String plainXml = workingFile.text + String encryptedXml = tool.encryptLoginIdentityProviders(plainXml, KEY_HEX) + logger.info("Encrypted XML: \n${encryptedXml}") + + // Act + def serializedLines = tool.serializeLoginIdentityProvidersAndPreserveFormat(encryptedXml, workingFile) + logger.info("Serialized lines: \n${serializedLines.join("\n")}") + + // Assert + + // Some empty lines will be removed + def trimmedLines = lines.collect {it.trim() }.findAll { it } + def trimmedSerializedLines = serializedLines.collect { it.trim() }.findAll { it } + assert trimmedLines.size() == trimmedSerializedLines.size() + } + + @Test + void testShouldPerformFullOperationForNiFiPropertiesAndLoginIdentityProviders() { + // 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 originalBootstrapLines = bootstrapFile.readLines() + String originalKeyLine = originalBootstrapLines.find { + it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) + } + logger.info("Original key line from bootstrap.conf: ${originalKeyLine}") + assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + + final String EXPECTED_KEY_LINE = ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + 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") + ProtectedNiFiProperties protectedInputProperties = new ProtectedNiFiProperties(inputProperties) + def originalSensitiveValues = protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key -> [(key): protectedInputProperties.getProperty(key)] } + logger.info("Original sensitive values: ${originalSensitiveValues}") + + // 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 originalXmlContent = inputLIPFile.text + logger.info("Original XML content: ${originalXmlContent}") + + String[] args = ["-n", inputPropertiesFile.path, "-l", inputLIPFile.path, "-b", bootstrapFile.path, "-i", outputLIPFile.path, "-o", outputPropertiesFile.path, "-k", KEY_HEX, "-v"] + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + exit.checkAssertionAfterwards(new Assertion() { + public void checkAssertion() { + final List 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() + originalSensitiveValues.every { String key, String originalValue -> + assert updatedProperties.getProperty(key) != originalValue + } + + // 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()) + } + + final String updatedXmlContent = outputLIPFile.text + logger.info("Updated XML content: ${updatedXmlContent}") + + // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) + def originalParsedXml = new XmlSlurper().parseText(originalXmlContent) + def updatedParsedXml = new XmlSlurper().parseText(updatedXmlContent) + assert originalParsedXml != updatedParsedXml + assert originalParsedXml.'**'.findAll { it.@encryption } != updatedParsedXml.'**'.findAll { + it.@encryption + } + + def encryptedValues = updatedParsedXml.provider.find { + it.identifier == 'ldap-provider' + }.property.findAll { + it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}" + } + + encryptedValues.each { + assert spp.unprotect(it.text()) == PASSWORD + } + + // Check that the comments are still there + def trimmedLines = inputLIPFile.readLines().collect {it.trim() }.findAll { it } + def trimmedSerializedLines = updatedXmlContent.split("\n").collect { it.trim() }.findAll { it } + assert trimmedLines.size() == trimmedSerializedLines.size() + + // Check that the key was persisted to the bootstrap.conf + final List updatedBootstrapLines = bootstrapFile.readLines() + String updatedKeyLine = updatedBootstrapLines.find { + it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) + } + logger.info("Updated key line: ${updatedKeyLine}") + + assert updatedKeyLine == EXPECTED_KEY_LINE + assert originalBootstrapLines.size() == updatedBootstrapLines.size() + + // Clean up + outputPropertiesFile.deleteOnExit() + outputLIPFile.deleteOnExit() + bootstrapFile.deleteOnExit() + tmpDir.deleteOnExit() + } + }); + + // Act + ConfigEncryptionTool.main(args) + logger.info("Invoked #main with ${args.join(" ")}") + + // Assert + + // Assertions defined above + } } public class TestAppender extends AppenderBase { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/bootstrap.conf b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_master_key_128.conf old mode 100755 new mode 100644 similarity index 95% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/bootstrap.conf rename to nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_master_key_128.conf index 9225126caadc..ae994deac3da --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/unreadable_conf/bootstrap.conf +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_master_key_128.conf @@ -52,7 +52,7 @@ java.arg.13=-XX:+UseG1GC java.arg.14=-Djava.awt.headless=true # Master key in hexadecimal format for encrypted sensitive configuration values -nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 +nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA9876543210 ### # Notification Services for notifying interested parties when NiFi is stopped, started, dies diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented-populated.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented-populated.xml new file mode 100644 index 000000000000..53f57369ba87 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented-populated.xml @@ -0,0 +1,107 @@ + + + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented.xml new file mode 100644 index 000000000000..a2beb4ce81f7 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-commented.xml @@ -0,0 +1,107 @@ + + + + + + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-empty.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-empty.xml new file mode 100644 index 000000000000..2a0917d3e756 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-empty.xml @@ -0,0 +1,105 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + someuser + thisIsABadPassword + + + + + + + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiline.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiline.xml new file mode 100644 index 000000000000..8c5ade51250c --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiline.xml @@ -0,0 +1,110 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + someuser + q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA + + + + Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g + + + + thisIsABadPassword + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiple-per-line.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiple-per-line.xml new file mode 100644 index 000000000000..e551035cce20 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted-multiple-per-line.xml @@ -0,0 +1,101 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + someuser + q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klAUah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g + + thisIsABadPassword + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted.xml new file mode 100644 index 000000000000..705eb88f3645 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-encrypted.xml @@ -0,0 +1,105 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + someuser + q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA + + + Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g + + + thisIsABadPassword + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiline.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiline.xml new file mode 100644 index 000000000000..b9f64a9e6770 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiline.xml @@ -0,0 +1,111 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + + + thisIsABadPassword + + + + thisIsABadPassword + + + + + thisIsABadPassword + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiple-per-line.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiple-per-line.xml new file mode 100644 index 000000000000..25148634d08f --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-multiple-per-line.xml @@ -0,0 +1,101 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + someuser + thisIsABadPasswordthisIsABadPassword + + thisIsABadPassword + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-single-line.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-single-line.xml new file mode 100644 index 000000000000..d20816467f76 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated-single-line.xml @@ -0,0 +1,101 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + thisIsABadPassword + + + thisIsABadPassword + thisIsABadPassword + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated.xml new file mode 100644 index 000000000000..5522c7362a31 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers-populated.xml @@ -0,0 +1,105 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + someuser + thisIsABadPassword + + + thisIsABadPassword + + + thisIsABadPassword + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers.xml new file mode 100644 index 000000000000..7e2a3e1c17d4 --- /dev/null +++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/login-identity-providers.xml @@ -0,0 +1,105 @@ + + + + + + + ldap-provider + org.apache.nifi.ldap.LdapProvider + START_TLS + + + + + + + + + + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + 12 hours + + + + + \ No newline at end of file