http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy ---------------------------------------------------------------------- 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 new file mode 100644 index 0000000..3b06c40 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy @@ -0,0 +1,463 @@ +/* + * 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.properties + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.util.encoders.DecoderException +import org.bouncycastle.util.encoders.Hex +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.BeforeClass +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 javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.SecureRandom +import java.security.Security + +@RunWith(JUnit4.class) +class AESSensitivePropertyProviderTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderTest.class) + + private static final String KEY_128_HEX = "0123456789ABCDEFFEDCBA9876543210" + private static final String KEY_256_HEX = KEY_128_HEX * 2 + private static final int IV_LENGTH = AESSensitivePropertyProvider.getIvLength() + + private static final List<Integer> KEY_SIZES = getAvailableKeySizes() + + private static final SecureRandom secureRandom = new SecureRandom() + + private static final Base64.Encoder encoder = Base64.encoder + private static final Base64.Decoder decoder = Base64.decoder + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + private static Cipher getCipher(boolean encrypt = true, int keySize = 256, byte[] iv = [0x00] * IV_LENGTH) { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding") + String key = getKeyOfSize(keySize) + cipher.init((encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, new SecretKeySpec(Hex.decode(key), "AES"), new IvParameterSpec(iv)) + logger.setup("Initialized a cipher in ${encrypt ? "encrypt" : "decrypt"} mode with a key of length ${keySize} bits") + cipher + } + + private static String getKeyOfSize(int keySize = 256) { + switch (keySize) { + case 128: + return KEY_128_HEX + case 192: + case 256: + if (Cipher.getMaxAllowedKeyLength("AES") < keySize) { + throw new IllegalArgumentException("The JCE unlimited strength cryptographic jurisdiction policies are not installed, so the max key size is 128 bits") + } + return KEY_256_HEX[0..<(keySize / 4)] + default: + throw new IllegalArgumentException("Key size ${keySize} bits is not valid") + } + } + + private static List<Integer> getAvailableKeySizes() { + if (Cipher.getMaxAllowedKeyLength("AES") > 128) { + [128, 192, 256] + } else { + [128] + } + } + + private static String manipulateString(String input, int start = 0, int end = input?.length()) { + if ((input[start..end] as List).unique().size() == 1) { + throw new IllegalArgumentException("Can't manipulate a String where the entire range is identical [${input[start..end]}]") + } + List shuffled = input[start..end] as List + Collections.shuffle(shuffled) + String reconstituted = input[0..<start] + shuffled.join() + input[end + 1..-1] + return reconstituted != input ? reconstituted : manipulateString(input, start, end) + } + + @Test + public void testShouldThrowExceptionOnInitializationWithoutBouncyCastle() throws Exception { + // Arrange + try { + Security.removeProvider(new BouncyCastleProvider().getName()) + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(KEY_128_HEX)) + logger.error("This should not be reached") + } + + // Assert + assert msg =~ "Error initializing the protection cipher" + } finally { + Security.addProvider(new BouncyCastleProvider()) + } + } + + // TODO: testShouldGetName() + + @Test + public void testShouldProtectValue() throws Exception { + final String PLAINTEXT = "This is a plaintext value" + + // Act + Map<Integer, String> CIPHER_TEXTS = KEY_SIZES.collectEntries { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + [(keySize): spp.protect(PLAINTEXT)] + } + CIPHER_TEXTS.each { ks, ct -> logger.info("Encrypted for ${ks} length key: ${ct}") } + + // Assert + + // The IV generation is part of #protect, so the expected cipher text values must be generated after #protect has run + Map<Integer, Cipher> decryptionCiphers = CIPHER_TEXTS.collectEntries { int keySize, String cipherText -> + // The 12 byte IV is the first 16 Base64-encoded characters of the "complete" cipher text + byte[] iv = decoder.decode(cipherText[0..<16]) + [(keySize): getCipher(false, keySize, iv)] + } + Map<Integer, String> plaintexts = decryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e -> + String cipherTextWithoutIVAndDelimiter = CIPHER_TEXTS[e.key][18..-1] + String plaintext = new String(e.value.doFinal(decoder.decode(cipherTextWithoutIVAndDelimiter)), StandardCharsets.UTF_8) + [(e.key): plaintext] + } + CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") } + + assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT } + } + + @Test + public void testShouldHandleProtectEmptyValue() throws Exception { + final List<String> EMPTY_PLAINTEXTS = ["", " ", null] + + // Act + KEY_SIZES.collectEntries { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + EMPTY_PLAINTEXTS.each { String emptyPlaintext -> + def msg = shouldFail(IllegalArgumentException) { + spp.protect(emptyPlaintext) + } + logger.expected("${msg} for keySize ${keySize} and plaintext [${emptyPlaintext}]") + + // Assert + assert msg == "Cannot encrypt an empty value" + } + } + } + + @Test + public void testShouldUnprotectValue() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { int keySize -> + byte[] iv = new byte[IV_LENGTH] + secureRandom.nextBytes(iv) + [(keySize): getCipher(true, keySize, iv)] + } + + Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> 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<Integer, String> 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(cipherText)] + } + plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") } + + // Assert + assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT } + } + + /** + * Tests inputs where the entire String is empty/blank space/{@code null}. + * + * @throws Exception + */ + @Test + public void testShouldHandleUnprotectEmptyValue() throws Exception { + // Arrange + final List<String> EMPTY_CIPHER_TEXTS = ["", " ", null] + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + EMPTY_CIPHER_TEXTS.each { String emptyCipherText -> + def msg = shouldFail(IllegalArgumentException) { + spp.unprotect(emptyCipherText) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]") + + // Assert + assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString() + } + } + } + + @Test + public void testShouldHandleUnprotectMalformedValue() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + String cipherText = spp.protect(PLAINTEXT) + // Swap two characters in the cipher text + final String MALFORMED_CIPHER_TEXT = manipulateString(cipherText, 25, 28) + logger.info("Manipulated ${cipherText} to\n${MALFORMED_CIPHER_TEXT.padLeft(163)}") + + def msg = shouldFail(SensitivePropertyProtectionException) { + spp.unprotect(MALFORMED_CIPHER_TEXT) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_CIPHER_TEXT}]") + + // Assert + assert msg == "Error decrypting a protected value" + } + } + + @Test + public void testShouldHandleUnprotectMissingIV() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + String cipherText = spp.protect(PLAINTEXT) + // Remove the IV from the "complete" cipher text + final String MISSING_IV_CIPHER_TEXT = cipherText[18..-1] + logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT.padLeft(163)}") + + def msg = shouldFail(IllegalArgumentException) { + spp.unprotect(MISSING_IV_CIPHER_TEXT) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT}]") + + // Remove the IV from the "complete" cipher text but keep the delimiter + final String MISSING_IV_CIPHER_TEXT_WITH_DELIMITER = cipherText[16..-1] + logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER.padLeft(163)}") + + def msgWithDelimiter = shouldFail(DecoderException) { + spp.unprotect(MISSING_IV_CIPHER_TEXT_WITH_DELIMITER) + } + logger.expected("${msgWithDelimiter} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER}]") + + // Assert + assert msg == "The cipher text does not contain the delimiter || -- it should be of the form Base64(IV) || Base64(cipherText)" + + // Assert + assert msgWithDelimiter =~ "unable to decode base64 string" + } + } + + /** + * Tests inputs which have a valid IV and delimiter but no "cipher text". + * + * @throws Exception + */ + @Test + public void testShouldHandleUnprotectEmptyCipherText() throws Exception { + // Arrange + final String IV_AND_DELIMITER = "${encoder.encodeToString("Bad IV value".getBytes(StandardCharsets.UTF_8))}||" + logger.info("IV and delimiter: ${IV_AND_DELIMITER}") + + final List<String> EMPTY_CIPHER_TEXTS = ["", " ", "\n"].collect { "${IV_AND_DELIMITER}${it}" } + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + EMPTY_CIPHER_TEXTS.each { String emptyCipherText -> + def msg = shouldFail(IllegalArgumentException) { + spp.unprotect(emptyCipherText) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]") + + // Assert + assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString() + } + } + } + + @Test + public void testShouldHandleUnprotectMalformedIV() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + String cipherText = spp.protect(PLAINTEXT) + // Swap two characters in the IV + final String MALFORMED_IV_CIPHER_TEXT = manipulateString(cipherText, 8, 11) + logger.info("Manipulated ${cipherText} to\n${MALFORMED_IV_CIPHER_TEXT.padLeft(163)}") + + def msg = shouldFail(SensitivePropertyProtectionException) { + spp.unprotect(MALFORMED_IV_CIPHER_TEXT) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_IV_CIPHER_TEXT}]") + + // Assert + assert msg == "Error decrypting a protected value" + } + } + + @Test + public void testShouldGetImplementationKeyWithDifferentMaxKeyLengths() throws Exception { + // Arrange + final int MAX_KEY_SIZE = getAvailableKeySizes().max() + final String EXPECTED_IMPL_KEY = "aes/gcm/${MAX_KEY_SIZE}" + logger.expected("Implementation key: ${EXPECTED_IMPL_KEY}") + + // Act + String key = new AESSensitivePropertyProvider(getKeyOfSize(MAX_KEY_SIZE)).getIdentifierKey() + logger.info("Implementation key: ${key}") + + // Assert + assert key == EXPECTED_IMPL_KEY + } + + @Test + public void testShouldNotAllowEmptyKey() throws Exception { + // Arrange + final String INVALID_KEY = "" + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY) + } + + // Assert + assert msg == "The key cannot be empty" + } + + @Test + public void testShouldNotAllowIncorrectlySizedKey() throws Exception { + // Arrange + final String INVALID_KEY = "Z" * 31 + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY) + } + + // Assert + assert msg == "The key must be a valid hexadecimal key" + } + + @Test + public void testShouldNotAllowInvalidKey() throws Exception { + // Arrange + final String INVALID_KEY = "Z" * 32 + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY) + } + + // Assert + assert msg == "The key must be a valid hexadecimal key" + } + + /** + * This test is to ensure internal consistency and allow for encrypting value for various property files + */ + @Test + public void testShouldEncryptArbitraryValues() { + // Arrange + def values = ["thisIsABadSensitiveKeyPassword", "thisIsABadKeystorePassword", "thisIsABadKeyPassword", "thisIsABadTruststorePassword", "This is an encrypted banner message"] + + String key = getKeyOfSize(128) + // key = "0" * 64 + + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key) + + // Act + def encryptedValues = values.collect { String v -> + def encryptedValue = spp.protect(v) + logger.info("${v} -> ${encryptedValue}") + def (String iv, String cipherText) = encryptedValue.tokenize("||") + logger.info("Normal Base64 encoding would be ${encoder.encodeToString(decoder.decode(iv))}||${encoder.encodeToString(decoder.decode(cipherText))}") + encryptedValue + } + + // Assert + assert values == encryptedValues.collect { spp.unprotect(it) } + } + + /** + * This test is to ensure external compatibility in case someone encodes the encrypted value with Base64 and does not remove the padding + */ + @Test + public void testShouldDecryptPaddedValue() { + // Arrange + Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128) + + final String EXPECTED_VALUE = "thisIsABadKeyPassword" + String cipherText = "ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg==" + String unpaddedCipherText = cipherText.replaceAll("=", "") + + String key = getKeyOfSize(256) + + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key) + + // Act + String rawValue = spp.unprotect(cipherText) + logger.info("Decrypted ${cipherText} to ${rawValue}") + String rawUnpaddedValue = spp.unprotect(unpaddedCipherText) + logger.info("Decrypted ${unpaddedCipherText} to ${rawUnpaddedValue}") + + // Assert + assert rawValue == EXPECTED_VALUE + assert rawUnpaddedValue == EXPECTED_VALUE + } +}
http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy ---------------------------------------------------------------------- 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 new file mode 100644 index 0000000..e1a0c65 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy @@ -0,0 +1,393 @@ +/* + * 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.properties + +import org.apache.nifi.util.NiFiProperties +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.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.nio.file.Files +import java.nio.file.attribute.PosixFilePermission +import java.security.Security + +@RunWith(JUnit4.class) +class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(NiFiPropertiesLoaderGroovyTest.class) + + final def DEFAULT_SENSITIVE_PROPERTIES = [ + "nifi.sensitive.props.key", + "nifi.security.keystorePasswd", + "nifi.security.keyPasswd", + "nifi.security.truststorePasswd" + ] + + final def COMMON_ADDITIONAL_SENSITIVE_PROPERTIES = [ + "nifi.sensitive.props.algorithm", + "nifi.kerberos.service.principal", + "nifi.kerberos.krb5.file", + "nifi.kerberos.keytab.location" + ] + + private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" * 2 + + private static String originalPropertiesPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) + private + final Set<PosixFilePermission> ownerReadWrite = [PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_READ] + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + // Clear the sensitive property providers between runs +// if (ProtectedNiFiProperties.@localProviderCache) { +// ProtectedNiFiProperties.@localProviderCache = [:] +// } + NiFiPropertiesLoader.@sensitivePropertyProviderFactory = null + } + + @AfterClass + public static void tearDownOnce() { + if (originalPropertiesPath) { + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, originalPropertiesPath) + } + } + + @Test + public void testConstructorShouldCreateNewInstance() throws Exception { + // Arrange + + // Act + NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader() + + // Assert + assert !niFiPropertiesLoader.@instance + assert !niFiPropertiesLoader.@keyHex + } + + @Test + public void testShouldCreateInstanceWithKey() throws Exception { + // Arrange + + // Act + NiFiPropertiesLoader niFiPropertiesLoader = NiFiPropertiesLoader.withKey(KEY_HEX) + + // Assert + assert !niFiPropertiesLoader.@instance + assert niFiPropertiesLoader.@keyHex == KEY_HEX + } + + @Test + public void testShouldGetDefaultProviderKey() throws Exception { + // Arrange + final String EXPECTED_PROVIDER_KEY = "aes/gcm/${Cipher.getMaxAllowedKeyLength("AES") > 128 ? 256 : 128}" + logger.info("Expected provider key: ${EXPECTED_PROVIDER_KEY}") + + // Act + String defaultKey = NiFiPropertiesLoader.getDefaultProviderKey() + logger.info("Default key: ${defaultKey}") + // Assert + assert defaultKey == EXPECTED_PROVIDER_KEY + } + + @Test + public void testShouldInitializeSensitivePropertyProviderFactory() throws Exception { + // Arrange + NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader() + + // Act + niFiPropertiesLoader.initializeSensitivePropertyProviderFactory() + + // Assert + assert niFiPropertiesLoader.@sensitivePropertyProviderFactory + } + + @Test + public void testShouldLoadUnprotectedPropertiesFromFile() throws Exception { + // Arrange + File unprotectedFile = new File("src/test/resources/conf/nifi.properties") + NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader() + + // Act + NiFiProperties niFiProperties = niFiPropertiesLoader.load(unprotectedFile) + + // Assert + assert niFiProperties.size() > 0 + + // Ensure it is not a ProtectedNiFiProperties + assert niFiProperties instanceof StandardNiFiProperties + } + + @Test + public void testShouldNotLoadUnprotectedPropertiesFromNullFile() throws Exception { + // Arrange + NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader() + + // Act + def msg = shouldFail(IllegalArgumentException) { + NiFiProperties niFiProperties = niFiPropertiesLoader.load(null as File) + } + logger.expected(msg) + + // Assert + assert msg == "NiFi properties file missing or unreadable" + } + + @Test + public void testShouldNotLoadUnprotectedPropertiesFromMissingFile() throws Exception { + // Arrange + File missingFile = new File("src/test/resources/conf/nifi_missing.properties") + assert !missingFile.exists() + + NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader() + + // Act + def msg = shouldFail(IllegalArgumentException) { + NiFiProperties niFiProperties = niFiPropertiesLoader.load(missingFile) + } + logger.expected(msg) + + // Assert + assert msg == "NiFi properties file missing or unreadable" + } + + @Test + public void testShouldNotLoadUnprotectedPropertiesFromUnreadableFile() throws Exception { + // Arrange + File unreadableFile = new File("src/test/resources/conf/nifi_no_permissions.properties") + Files.setPosixFilePermissions(unreadableFile.toPath(), [] as Set) + assert !unreadableFile.canRead() + + NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader() + + // Act + def msg = shouldFail(IllegalArgumentException) { + NiFiProperties niFiProperties = niFiPropertiesLoader.load(unreadableFile) + } + logger.expected(msg) + + // Assert + assert msg == "NiFi properties file missing or unreadable" + + // Clean up to allow for indexing, etc. + Files.setPosixFilePermissions(unreadableFile.toPath(), ownerReadWrite) + } + + @Test + public void testShouldLoadUnprotectedPropertiesFromPath() throws Exception { + // Arrange + File unprotectedFile = new File("src/test/resources/conf/nifi.properties") + NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader() + + // Act + NiFiProperties niFiProperties = niFiPropertiesLoader.load(unprotectedFile.path) + + // Assert + assert niFiProperties.size() > 0 + + // Ensure it is not a ProtectedNiFiProperties + assert niFiProperties instanceof StandardNiFiProperties + } + + @Test + public void testShouldLoadUnprotectedPropertiesFromProtectedFile() throws Exception { + // Arrange + File protectedFile = new File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes.properties") + NiFiPropertiesLoader niFiPropertiesLoader = NiFiPropertiesLoader.withKey(KEY_HEX) + + final def EXPECTED_PLAIN_VALUES = [ + (NiFiProperties.SENSITIVE_PROPS_KEY): "thisIsABadSensitiveKeyPassword", + (NiFiProperties.SECURITY_KEYSTORE_PASSWD): "thisIsABadKeystorePassword", + (NiFiProperties.SECURITY_KEY_PASSWD): "thisIsABadKeyPassword", + ] + + // This method is covered in tests above, so safe to use here to retrieve protected properties + ProtectedNiFiProperties protectedNiFiProperties = niFiPropertiesLoader.readProtectedPropertiesFromDisk(protectedFile) + int totalKeysCount = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes().size() + int protectedKeysCount = protectedNiFiProperties.getProtectedPropertyKeys().size() + logger.info("Read ${totalKeysCount} total properties (${protectedKeysCount} protected) from ${protectedFile.canonicalPath}") + + // Act + NiFiProperties niFiProperties = niFiPropertiesLoader.load(protectedFile) + + // Assert + assert niFiProperties.size() == totalKeysCount - protectedKeysCount + + // Ensure that any key marked as protected above is different in this instance + protectedNiFiProperties.getProtectedPropertyKeys().keySet().each { String key -> + String plainValue = niFiProperties.getProperty(key) + String protectedValue = protectedNiFiProperties.getProperty(key) + + logger.info("Checking that [${protectedValue}] -> [${plainValue}] == [${EXPECTED_PLAIN_VALUES[key]}]") + + assert plainValue == EXPECTED_PLAIN_VALUES[key] + assert plainValue != protectedValue + assert plainValue.length() <= protectedValue.length() + } + + // Ensure it is not a ProtectedNiFiProperties + assert niFiProperties instanceof StandardNiFiProperties + } + + @Test + public void testShouldExtractKeyFromBootstrapFile() throws Exception { + // Arrange + def defaultNiFiPropertiesFilePath = "src/test/resources/bootstrap_tests/conf/nifi.properties" + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath) + + // Act + String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile() + + // Assert + assert key == KEY_HEX + } + + @Test + public void testShouldNotExtractKeyFromBootstrapFileWithoutKeyLine() throws Exception { + // Arrange + def defaultNiFiPropertiesFilePath = "src/test/resources/bootstrap_tests/missing_key_line/nifi.properties" + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath) + + // Act + String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile() + + // Assert + assert key == "" + } + + @Test + public void testShouldNotExtractKeyFromBootstrapFileWithoutKey() throws Exception { + // Arrange + def defaultNiFiPropertiesFilePath = "src/test/resources/bootstrap_tests/missing_key_line/nifi.properties" + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath) + + // Act + String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile() + + // Assert + assert key == "" + } + + @Test + public void testShouldNotExtractKeyFromMissingBootstrapFile() throws Exception { + // Arrange + def defaultNiFiPropertiesFilePath = "src/test/resources/bootstrap_tests/missing_bootstrap/nifi.properties" + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath) + + // Act + def msg = shouldFail(IOException) { + String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile() + } + logger.expected(msg) + + // Assert + assert msg == "Cannot read from bootstrap.conf" + } + + @Test + public void testShouldNotExtractKeyFromUnreadableBootstrapFile() throws Exception { + // Arrange + File unreadableFile = new File("src/test/resources/bootstrap_tests/unreadable_bootstrap/bootstrap.conf") + Set<PosixFilePermission> originalPermissions = Files.getPosixFilePermissions(unreadableFile.toPath()) + Files.setPosixFilePermissions(unreadableFile.toPath(), [] as Set) + assert !unreadableFile.canRead() + + def defaultNiFiPropertiesFilePath = "src/test/resources/bootstrap_tests/unreadable_bootstrap/nifi.properties" + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath) + + // Act + def msg = shouldFail(IOException) { + String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile() + } + logger.expected(msg) + + // Assert + assert msg == "Cannot read from bootstrap.conf" + + // Clean up to allow for indexing, etc. + Files.setPosixFilePermissions(unreadableFile.toPath(), originalPermissions) + } + + @Test + public void testShouldNotExtractKeyFromUnreadableConfDir() throws Exception { + // Arrange + File unreadableDir = new File("src/test/resources/bootstrap_tests/unreadable_conf") + Set<PosixFilePermission> originalPermissions = Files.getPosixFilePermissions(unreadableDir.toPath()) + Files.setPosixFilePermissions(unreadableDir.toPath(), [] as Set) + assert !unreadableDir.canRead() + + def defaultNiFiPropertiesFilePath = "src/test/resources/bootstrap_tests/unreadable_conf/nifi.properties" + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath) + + // Act + def msg = shouldFail(IOException) { + String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile() + } + logger.expected(msg) + + // Assert + assert msg == "Cannot read from bootstrap.conf" + + // Clean up to allow for indexing, etc. + Files.setPosixFilePermissions(unreadableDir.toPath(), originalPermissions) + } + + @Test + public void testShouldLoadUnprotectedPropertiesFromProtectedDefaultFileAndUseBootstrapKey() throws Exception { + // Arrange + File protectedFile = new File("src/test/resources/bootstrap_tests/conf/nifi_with_sensitive_properties_protected_aes.properties") + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, protectedFile.path) + NiFiPropertiesLoader niFiPropertiesLoader = NiFiPropertiesLoader.withKey(KEY_HEX) + + NiFiProperties normalReadProperties = niFiPropertiesLoader.load(protectedFile) + logger.info("Read ${normalReadProperties.size()} total properties from ${protectedFile.canonicalPath}") + + // Act + NiFiProperties niFiProperties = NiFiPropertiesLoader.loadDefaultWithKeyFromBootstrap() + + // Assert + assert niFiProperties.size() == normalReadProperties.size() + + + def readPropertiesAndValues = niFiProperties.getPropertyKeys().collectEntries { + [(it): niFiProperties.getProperty(it)] + } + def expectedPropertiesAndValues = normalReadProperties.getPropertyKeys().collectEntries { + [(it): normalReadProperties.getProperty(it)] + } + assert readPropertiesAndValues == expectedPropertiesAndValues + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy new file mode 100644 index 0000000..bf4e677 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy @@ -0,0 +1,860 @@ +/* + * 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.properties + +import org.apache.nifi.util.NiFiProperties +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.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.security.Security + +@RunWith(JUnit4.class) +class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiPropertiesGroovyTest.class) + + final def DEFAULT_SENSITIVE_PROPERTIES = [ + "nifi.sensitive.props.key", + "nifi.security.keystorePasswd", + "nifi.security.keyPasswd", + "nifi.security.truststorePasswd" + ] + + final def COMMON_ADDITIONAL_SENSITIVE_PROPERTIES = [ + "nifi.sensitive.props.algorithm", + "nifi.kerberos.service.principal", + "nifi.kerberos.krb5.file", + "nifi.kerberos.keytab.location" + ] + + private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" * 2 + + private static String originalPropertiesPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @AfterClass + public static void tearDownOnce() { + if (originalPropertiesPath) { + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, originalPropertiesPath) + } + } + + private static ProtectedNiFiProperties loadFromFile(String propertiesFilePath) { + String filePath + try { + filePath = ProtectedNiFiPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath() + } catch (URISyntaxException ex) { + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex) + } + + File file = new File(filePath) + + if (file == null || !file.exists() || !file.canRead()) { + String path = (file == null ? "missing file" : file.getAbsolutePath()) + logger.error("Cannot read from '{}' -- file is missing or not readable", path) + throw new IllegalArgumentException("NiFi properties file missing or unreadable") + } + + Properties rawProperties = new Properties() + + InputStream inStream = null + try { + inStream = new BufferedInputStream(new FileInputStream(file)) + rawProperties.load(inStream) + logger.info("Loaded {} properties from {}", rawProperties.size(), file.getAbsolutePath()) + + ProtectedNiFiProperties protectedNiFiProperties = new ProtectedNiFiProperties(rawProperties) + + // If it has protected keys, inject the SPP + if (protectedNiFiProperties.hasProtectedKeys()) { + protectedNiFiProperties.addSensitivePropertyProvider(new AESSensitivePropertyProvider(KEY_HEX)) + } + + return protectedNiFiProperties + } catch (final Exception ex) { + logger.error("Cannot load properties file due to " + ex.getLocalizedMessage()) + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex) + } finally { + if (null != inStream) { + try { + inStream.close() + } catch (final Exception ex) { + /** + * do nothing * + */ + } + } + } + } + + @Test + public void testConstructorShouldCreateNewInstance() throws Exception { + // Arrange + + // Act + NiFiProperties niFiProperties = new StandardNiFiProperties() + logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}") + + // Assert + assert niFiProperties.size() == 0 + assert niFiProperties.getPropertyKeys() == [] as Set + } + + @Test + public void testConstructorShouldAcceptRawProperties() throws Exception { + // Arrange + Properties rawProperties = new Properties() + rawProperties.setProperty("key", "value") + logger.info("rawProperties has ${rawProperties.size()} properties: ${rawProperties.stringPropertyNames()}") + assert rawProperties.size() == 1 + + // Act + NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties) + logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}") + + // Assert + assert niFiProperties.size() == 1 + assert niFiProperties.getPropertyKeys() == ["key"] as Set + } + + @Test + public void testConstructorShouldAcceptNiFiProperties() throws Exception { + // Arrange + Properties rawProperties = new Properties() + rawProperties.setProperty("key", "value") + rawProperties.setProperty("key.protected", "value2") + NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties) + logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}") + assert niFiProperties.size() == 2 + + // Act + ProtectedNiFiProperties protectedNiFiProperties = new ProtectedNiFiProperties(niFiProperties) + logger.info("protectedNiFiProperties has ${protectedNiFiProperties.size()} properties: ${protectedNiFiProperties.getPropertyKeys()}") + + // Assert + def allKeys = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes() + assert allKeys == ["key", "key.protected"] as Set + assert allKeys.size() == niFiProperties.size() + + } + + @Test + public void testShouldAllowMultipleInstances() throws Exception { + // Arrange + Properties rawProperties = new Properties() + rawProperties.setProperty("key", "value") + logger.info("rawProperties has ${rawProperties.size()} properties: ${rawProperties.stringPropertyNames()}") + assert rawProperties.size() == 1 + + // Act + NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties) + logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}") + NiFiProperties emptyProperties = new StandardNiFiProperties() + logger.info("emptyProperties has ${emptyProperties.size()} properties: ${emptyProperties.getPropertyKeys()}") + + // Assert + assert niFiProperties.size() == 1 + assert niFiProperties.getPropertyKeys() == ["key"] as Set + + assert emptyProperties.size() == 0 + assert emptyProperties.getPropertyKeys() == [] as Set + } + + @Test + public void testShouldDetectIfPropertyIsSensitive() throws Exception { + // Arrange + final String INSENSITIVE_PROPERTY_KEY = "nifi.ui.banner.text" + final String SENSITIVE_PROPERTY_KEY = "nifi.security.keystorePasswd" + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi.properties") + + // Act + boolean bannerIsSensitive = properties.isPropertySensitive(INSENSITIVE_PROPERTY_KEY) + logger.info("${INSENSITIVE_PROPERTY_KEY} is ${bannerIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + boolean passwordIsSensitive = properties.isPropertySensitive(SENSITIVE_PROPERTY_KEY) + logger.info("${SENSITIVE_PROPERTY_KEY} is ${passwordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + + // Assert + assert !bannerIsSensitive + assert passwordIsSensitive + } + + @Test + public void testShouldGetDefaultSensitiveProperties() throws Exception { + // Arrange + logger.expected("${DEFAULT_SENSITIVE_PROPERTIES.size()} default sensitive properties: ${DEFAULT_SENSITIVE_PROPERTIES.join(", ")}") + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi.properties") + + // Act + List defaultSensitiveProperties = properties.getSensitivePropertyKeys() + logger.info("${defaultSensitiveProperties.size()} default sensitive properties: ${defaultSensitiveProperties.join(", ")}") + + // Assert + assert defaultSensitiveProperties.size() == DEFAULT_SENSITIVE_PROPERTIES.size() + assert defaultSensitiveProperties.containsAll(DEFAULT_SENSITIVE_PROPERTIES) + } + + @Test + public void testShouldGetAdditionalSensitiveProperties() throws Exception { + // Arrange + def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + ["nifi.ui.banner.text", "nifi.version"] + logger.expected("${completeSensitiveProperties.size()} total sensitive properties: ${completeSensitiveProperties.join(", ")}") + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_additional_sensitive_keys.properties") + + // Act + List retrievedSensitiveProperties = properties.getSensitivePropertyKeys() + logger.info("${retrievedSensitiveProperties.size()} retrieved sensitive properties: ${retrievedSensitiveProperties.join(", ")}") + + // Assert + assert retrievedSensitiveProperties.size() == completeSensitiveProperties.size() + assert retrievedSensitiveProperties.containsAll(completeSensitiveProperties) + } + + // TODO: Add negative tests (fuzz additional.keys property, etc.) + + @Test + public void testGetAdditionalSensitivePropertiesShouldNotIncludeSelf() throws Exception { + // Arrange + def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + ["nifi.ui.banner.text", "nifi.version"] + logger.expected("${completeSensitiveProperties.size()} total sensitive properties: ${completeSensitiveProperties.join(", ")}") + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_additional_sensitive_keys.properties") + + // Act + List retrievedSensitiveProperties = properties.getSensitivePropertyKeys() + logger.info("${retrievedSensitiveProperties.size()} retrieved sensitive properties: ${retrievedSensitiveProperties.join(", ")}") + + // Assert + assert retrievedSensitiveProperties.size() == completeSensitiveProperties.size() + assert retrievedSensitiveProperties.containsAll(completeSensitiveProperties) + } + + /** + * In the default (no protection enabled) scenario, a call to retrieve a sensitive property should return the raw value transparently. + * @throws Exception + */ + @Test + public void testShouldGetUnprotectedValueOfSensitiveProperty() throws Exception { + // Arrange + final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd" + final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword" + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_unprotected.properties") + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + String retrievedKeystorePassword = properties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == EXPECTED_KEYSTORE_PASSWORD + assert isSensitive + assert !isProtected + } + + /** + * In the default (no protection enabled) scenario, a call to retrieve a sensitive property (which is empty) should return the raw value transparently. + * @throws Exception + */ + @Test + public void testShouldGetEmptyUnprotectedValueOfSensitiveProperty() throws Exception { + // Arrange + final String TRUSTSTORE_PASSWORD_KEY = "nifi.security.truststorePasswd" + final String EXPECTED_TRUSTSTORE_PASSWORD = "" + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_unprotected.properties") + + boolean isSensitive = properties.isPropertySensitive(TRUSTSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(TRUSTSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedTruststorePassword = unprotectedProperties.getProperty(TRUSTSTORE_PASSWORD_KEY) + logger.info("${TRUSTSTORE_PASSWORD_KEY}: ${retrievedTruststorePassword}") + + // Assert + assert retrievedTruststorePassword == EXPECTED_TRUSTSTORE_PASSWORD + assert isSensitive + assert !isProtected + } + + /** + * The new model no longer needs to maintain the protected state -- it is used as a wrapper/decorator during load to unprotect the sensitive properties and then return an instance of raw properties. + * + * @throws Exception + */ + @Test + public void testShouldGetUnprotectedValueOfSensitivePropertyWhenProtected() throws Exception { + // Arrange + final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd" + final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword" + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties") + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == EXPECTED_KEYSTORE_PASSWORD + assert isSensitive + assert isProtected + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is protected with an unknown protection scheme. + * @throws Exception + */ + @Test + public void testGetValueOfSensitivePropertyShouldHandleUnknownProtectionScheme() throws Exception { + // Arrange + final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd" + + // Raw properties + Properties rawProperties = new Properties() + rawProperties.load(new File("src/test/resources/conf/nifi_with_sensitive_properties_protected_unknown.properties").newInputStream()) + final String RAW_KEYSTORE_PASSWORD = rawProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: ${RAW_KEYSTORE_PASSWORD}") + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_unknown.properties") + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + + // While the value is "protected", the scheme is not registered, so treat it as raw + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == RAW_KEYSTORE_PASSWORD + assert isSensitive + assert isProtected + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is unable to be unprotected due to a malformed value. + * @throws Exception + */ + @Test + public void testGetValueOfSensitivePropertyShouldHandleSingleMalformedValue() throws Exception { + // Arrange + final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd" + + // Raw properties + Properties rawProperties = new Properties() + rawProperties.load(new File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes_single_malformed.properties").newInputStream()) + final String RAW_KEYSTORE_PASSWORD = rawProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: ${RAW_KEYSTORE_PASSWORD}") + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes_single_malformed.properties") + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + NiFiProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + } + logger.expected(msg) + + // Assert + assert msg =~ "Failed to unprotect key ${KEYSTORE_PASSWORD_KEY}" + assert isSensitive + assert isProtected + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is unable to be unprotected due to a malformed value. + * @throws Exception + */ + @Test + public void testGetValueOfSensitivePropertyShouldHandleMultipleMalformedValues() throws Exception { + // Arrange + + // Raw properties + Properties rawProperties = new Properties() + rawProperties.load(new File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes_multiple_malformed.properties").newInputStream()) + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes_multiple_malformed.properties") + + // Iterate over the protected keys and track the ones that fail to decrypt + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + Set<String> malformedKeys = properties.getProtectedPropertyKeys() + .findAll { String key, String scheme -> scheme == spp.identifierKey } + .keySet().collect { String key -> + try { + String rawValue = spp.unprotect(properties.getProperty(key)) + return + } catch (SensitivePropertyProtectionException e) { + logger.expected("Caught a malformed value for ${key}") + return key + } + } + + logger.expected("Malformed keys: ${malformedKeys.join(", ")}") + + // Act + def e = groovy.test.GroovyAssert.shouldFail(SensitivePropertyProtectionException) { + NiFiProperties unprotectedProperties = properties.getUnprotectedProperties() + } + logger.expected(e.getMessage()) + + // Assert + assert e instanceof MultipleSensitivePropertyProtectionException + assert e.getMessage() =~ "Failed to unprotect keys" + assert e.getFailedKeys() == malformedKeys + + } + + /** + * In the default (no protection enabled) scenario, a call to retrieve a sensitive property (which is empty) should return the raw value transparently. + * @throws Exception + */ + @Test + public void testShouldGetEmptyUnprotectedValueOfSensitivePropertyWithDefault() throws Exception { + // Arrange + final String TRUSTSTORE_PASSWORD_KEY = "nifi.security.truststorePasswd" + final String EXPECTED_TRUSTSTORE_PASSWORD = "" + final String DEFAULT_VALUE = "defaultValue" + + // Raw properties + Properties rawProperties = new Properties() + rawProperties.load(new File("src/test/resources/conf/nifi_with_sensitive_properties_unprotected.properties").newInputStream()) + final String RAW_TRUSTSTORE_PASSWORD = rawProperties.getProperty(TRUSTSTORE_PASSWORD_KEY) + logger.info("Raw value for ${TRUSTSTORE_PASSWORD_KEY}: ${RAW_TRUSTSTORE_PASSWORD}") + assert RAW_TRUSTSTORE_PASSWORD == EXPECTED_TRUSTSTORE_PASSWORD + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_unprotected.properties") + + boolean isSensitive = properties.isPropertySensitive(TRUSTSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(TRUSTSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + String retrievedTruststorePassword = properties.getProperty(TRUSTSTORE_PASSWORD_KEY, DEFAULT_VALUE) + logger.info("${TRUSTSTORE_PASSWORD_KEY}: ${retrievedTruststorePassword}") + + // Assert + assert retrievedTruststorePassword == DEFAULT_VALUE + assert isSensitive + assert !isProtected + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should return the raw value transparently. + * @throws Exception + */ + @Test + public void testShouldGetUnprotectedValueOfSensitivePropertyWhenProtectedWithDefault() throws Exception { + // Arrange + final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd" + final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword" + final String DEFAULT_VALUE = "defaultValue" + + // Raw properties + Properties rawProperties = new Properties() + rawProperties.load(new File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes.properties").newInputStream()) + final String RAW_KEYSTORE_PASSWORD = rawProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: ${RAW_KEYSTORE_PASSWORD}") + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties") + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY, DEFAULT_VALUE) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == EXPECTED_KEYSTORE_PASSWORD + assert isSensitive + assert isProtected + } + + // TODO: Test getProtected with multiple providers + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the internal cache of providers is empty. + * @throws Exception + */ + @Test + public void testGetValueOfSensitivePropertyShouldHandleInvalidatedInternalCache() throws Exception { + // Arrange + final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd" + final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword" + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties") + + final String RAW_PASSWORD = properties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("Read raw value from properties: ${RAW_PASSWORD}") + + // Overwrite the internal cache + properties.localProviderCache = [:] + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == RAW_PASSWORD + assert isSensitive + assert isProtected + } + + @Test + public void testShouldDetectIfPropertyIsProtected() throws Exception { + // Arrange + final String UNPROTECTED_PROPERTY_KEY = "nifi.security.truststorePasswd" + final String PROTECTED_PROPERTY_KEY = "nifi.security.keystorePasswd" + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties") + + // Act + boolean unprotectedPasswordIsSensitive = properties.isPropertySensitive(UNPROTECTED_PROPERTY_KEY) + boolean unprotectedPasswordIsProtected = properties.isPropertyProtected(UNPROTECTED_PROPERTY_KEY) + logger.info("${UNPROTECTED_PROPERTY_KEY} is ${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + logger.info("${UNPROTECTED_PROPERTY_KEY} is ${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}") + boolean protectedPasswordIsSensitive = properties.isPropertySensitive(PROTECTED_PROPERTY_KEY) + boolean protectedPasswordIsProtected = properties.isPropertyProtected(PROTECTED_PROPERTY_KEY) + logger.info("${PROTECTED_PROPERTY_KEY} is ${protectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + logger.info("${PROTECTED_PROPERTY_KEY} is ${protectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}") + + // Assert + assert unprotectedPasswordIsSensitive + assert !unprotectedPasswordIsProtected + + assert protectedPasswordIsSensitive + assert protectedPasswordIsProtected + } + + @Test + public void testShouldDetectIfPropertyWithEmptyProtectionSchemeIsProtected() throws Exception { + // Arrange + final String UNPROTECTED_PROPERTY_KEY = "nifi.sensitive.props.key" + + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_unprotected_extra_line.properties") + + // Act + boolean unprotectedPasswordIsSensitive = properties.isPropertySensitive(UNPROTECTED_PROPERTY_KEY) + boolean unprotectedPasswordIsProtected = properties.isPropertyProtected(UNPROTECTED_PROPERTY_KEY) + logger.info("${UNPROTECTED_PROPERTY_KEY} is ${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + logger.info("${UNPROTECTED_PROPERTY_KEY} is ${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}") + + // Assert + assert unprotectedPasswordIsSensitive + assert !unprotectedPasswordIsProtected + } + + @Test + public void testShouldGetPercentageOfSensitivePropertiesProtected_0() throws Exception { + // Arrange + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi.properties") + + logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}") + logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}") + + // Act + double percentProtected = properties.getPercentOfSensitivePropertiesProtected() + logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getSensitivePropertyKeys().size()}) protected") + + // Assert + assert percentProtected == 0.0 + } + + @Test + public void testShouldGetPercentageOfSensitivePropertiesProtected_50() throws Exception { + // Arrange + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties") + + logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}") + logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}") + + // Act + double percentProtected = properties.getPercentOfSensitivePropertiesProtected() + logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getSensitivePropertyKeys().size()}) protected") + + // Assert + assert percentProtected == 50.0 + } + + @Test + public void testShouldGetPercentageOfSensitivePropertiesProtected_100() throws Exception { + // Arrange + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_all_sensitive_properties_protected_aes.properties") + + logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}") + logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}") + + // Act + double percentProtected = properties.getPercentOfSensitivePropertiesProtected() + logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getSensitivePropertyKeys().size()}) protected") + + // Assert + assert percentProtected == 100.0 + } + + @Test + public void testInstanceWithNoProtectedPropertiesShouldNotLoadSPP() throws Exception { + // Arrange + ProtectedNiFiProperties properties = loadFromFile("/conf/nifi.properties") + assert properties.@localProviderCache?.isEmpty() + + logger.info("Has protected properties: ${properties.hasProtectedKeys()}") + assert !properties.hasProtectedKeys() + + // Act + Map localCache = properties.@localProviderCache + logger.info("Internal cache ${localCache} has ${localCache.size()} providers loaded") + + // Assert + assert localCache.isEmpty() + } + + @Test + public void testShouldAddSensitivePropertyProvider() throws Exception { + // Arrange + ProtectedNiFiProperties properties = new ProtectedNiFiProperties() + assert properties.getSensitivePropertyProviders().isEmpty() + + SensitivePropertyProvider mockProvider = + [unprotect : { String input -> + logger.mock("Mock call to #unprotect(${input})") + input.reverse() + }, + getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider + + // Act + properties.addSensitivePropertyProvider(mockProvider) + + // Assert + assert properties.getSensitivePropertyProviders().size() == 1 + } + + @Test + public void testShouldNotAddNullSensitivePropertyProvider() throws Exception { + // Arrange + ProtectedNiFiProperties properties = new ProtectedNiFiProperties() + assert properties.getSensitivePropertyProviders().isEmpty() + + // Act + def msg = shouldFail(IllegalArgumentException) { + properties.addSensitivePropertyProvider(null) + } + logger.expected(msg) + + // Assert + assert properties.getSensitivePropertyProviders().size() == 0 + assert msg == "Cannot add null SensitivePropertyProvider" + } + + @Test + public void testShouldNotAllowOverwriteOfProvider() throws Exception { + // Arrange + ProtectedNiFiProperties properties = new ProtectedNiFiProperties() + assert properties.getSensitivePropertyProviders().isEmpty() + + SensitivePropertyProvider mockProvider = + [unprotect : { String input -> + logger.mock("Mock call to 1#unprotect(${input})") + input.reverse() + }, + getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider + properties.addSensitivePropertyProvider(mockProvider) + assert properties.getSensitivePropertyProviders().size() == 1 + + SensitivePropertyProvider mockProvider2 = + [unprotect : { String input -> + logger.mock("Mock call to 2#unprotect(${input})") + input.reverse() + }, + getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider + + // Act + def msg = shouldFail(UnsupportedOperationException) { + properties.addSensitivePropertyProvider(mockProvider2) + } + logger.expected(msg) + + // Assert + assert msg == "Cannot overwrite existing sensitive property provider registered for mockProvider" + assert properties.getSensitivePropertyProviders().size() == 1 + } + + @Test + void testGetUnprotectedPropertiesShouldReturnInternalInstanceWhenNoneProtected() { + // Arrange + String noProtectedPropertiesPath = "/conf/nifi.properties" + ProtectedNiFiProperties protectedNiFiProperties = loadFromFile(noProtectedPropertiesPath) + logger.info("Loaded ${protectedNiFiProperties.size()} properties from ${noProtectedPropertiesPath}") + + int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode() + logger.info("Hash code of internal instance: ${hashCode}") + + // Act + NiFiProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties() + logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties") + + // Assert + assert unprotectedNiFiProperties.size() == protectedNiFiProperties.size() + assert unprotectedNiFiProperties.getPropertyKeys().every { + !unprotectedNiFiProperties.getProperty(it).endsWith(".protected") + } + logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}") + assert unprotectedNiFiProperties.hashCode() == hashCode + } + + @Test + void testGetUnprotectedPropertiesShouldDecryptProtectedProperties() { + // Arrange + String noProtectedPropertiesPath = "/conf/nifi_with_sensitive_properties_protected_aes.properties" + ProtectedNiFiProperties protectedNiFiProperties = loadFromFile(noProtectedPropertiesPath) + logger.info("Loaded ${protectedNiFiProperties.size()} properties from ${noProtectedPropertiesPath}") + + int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size() + int protectionSchemeCount = protectedNiFiProperties + .getPropertyKeys().findAll { it.endsWith(".protected") } + .size() + int expectedUnprotectedPropertyCount = protectedNiFiProperties.size() - protectionSchemeCount + + String protectedProps = protectedNiFiProperties + .getProtectedPropertyKeys() + .collectEntries { + [(it.key): protectedNiFiProperties.getProperty(it.key)] + }.entrySet() + .join("\n") + + logger.info("Detected ${protectedPropertyCount} protected properties and ${protectionSchemeCount} protection scheme properties") + logger.info("Protected properties: \n${protectedProps}") + + logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}") + + int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode() + logger.info("Hash code of internal instance: ${hashCode}") + + // Act + NiFiProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties() + logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties") + + // Assert + assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount + assert unprotectedNiFiProperties.getPropertyKeys().every { + !unprotectedNiFiProperties.getProperty(it).endsWith(".protected") + } + logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}") + assert unprotectedNiFiProperties.hashCode() != hashCode + } + + @Test + void testShouldCalculateSize() { + // Arrange + Properties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as Properties + ProtectedNiFiProperties protectedNiFiProperties = new ProtectedNiFiProperties(rawProperties) + logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}") + + // Act + int protectedSize = protectedNiFiProperties.size() + logger.info("Protected properties (${protectedNiFiProperties.size()}): ${protectedNiFiProperties.getPropertyKeys().join(", ")}") + + // Assert + assert protectedSize == rawProperties.size() - 1 + } + + @Test + void testGetPropertyKeysShouldMatchSize() { + // Arrange + Properties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as Properties + ProtectedNiFiProperties protectedNiFiProperties = new ProtectedNiFiProperties(rawProperties) + logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}") + + // Act + def filteredKeys = protectedNiFiProperties.getPropertyKeys() + logger.info("Protected properties (${protectedNiFiProperties.size()}): ${filteredKeys.join(", ")}") + + // Assert + assert protectedNiFiProperties.size() == rawProperties.size() - 1 + assert filteredKeys == rawProperties.keySet() - "key.protected" + } + + @Test + void testShouldGetPropertyKeysIncludingProtectionSchemes() { + // Arrange + Properties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as Properties + ProtectedNiFiProperties protectedNiFiProperties = new ProtectedNiFiProperties(rawProperties) + logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}") + + // Act + def allKeys = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes() + logger.info("Protected properties with schemes (${allKeys.size()}): ${allKeys.join(", ")}") + + // Assert + assert allKeys.size() == rawProperties.size() + assert allKeys == rawProperties.keySet() + } + + // TODO: Add tests for protectPlainProperties +} http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy new file mode 100644 index 0000000..c9492fb --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy @@ -0,0 +1,150 @@ +/* + * 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.properties + +import org.apache.nifi.util.NiFiProperties +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@RunWith(JUnit4.class) +class StandardNiFiPropertiesGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(StandardNiFiPropertiesGroovyTest.class) + + private static String originalPropertiesPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) + + @BeforeClass + public static void setUpOnce() throws Exception { + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @AfterClass + public static void tearDownOnce() { + if (originalPropertiesPath) { + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, originalPropertiesPath) + } + } + + private static StandardNiFiProperties loadFromFile(String propertiesFilePath) { + String filePath; + try { + filePath = StandardNiFiPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath(); + } catch (URISyntaxException ex) { + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex); + } + + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, filePath); + + StandardNiFiProperties properties = new StandardNiFiProperties(); + + // clear out existing properties + for (String prop : properties.stringPropertyNames()) { + properties.remove(prop); + } + + InputStream inStream = null; + try { + inStream = new BufferedInputStream(new FileInputStream(filePath)); + properties.load(inStream); + } catch (final Exception ex) { + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex); + } finally { + if (null != inStream) { + try { + inStream.close(); + } catch (Exception ex) { + /** + * do nothing * + */ + } + } + } + + return properties; + } + + @Test + public void testConstructorShouldCreateNewInstance() throws Exception { + // Arrange + + // Act + NiFiProperties niFiProperties = new StandardNiFiProperties() + logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}") + + // Assert + assert niFiProperties.size() == 0 + assert niFiProperties.getPropertyKeys() == [] as Set + } + + @Test + public void testConstructorShouldAcceptRawProperties() throws Exception { + // Arrange + Properties rawProperties = new Properties() + rawProperties.setProperty("key", "value") + logger.info("rawProperties has ${rawProperties.size()} properties: ${rawProperties.stringPropertyNames()}") + assert rawProperties.size() == 1 + + // Act + NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties) + logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}") + + // Assert + assert niFiProperties.size() == 1 + assert niFiProperties.getPropertyKeys() == ["key"] as Set + } + + @Test + public void testShouldAllowMultipleInstances() throws Exception { + // Arrange + Properties rawProperties = new Properties() + rawProperties.setProperty("key", "value") + logger.info("rawProperties has ${rawProperties.size()} properties: ${rawProperties.stringPropertyNames()}") + assert rawProperties.size() == 1 + + // Act + NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties) + logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}") + NiFiProperties emptyProperties = new StandardNiFiProperties() + logger.info("emptyProperties has ${emptyProperties.size()} properties: ${emptyProperties.getPropertyKeys()}") + + + // Assert + assert niFiProperties.size() == 1 + assert niFiProperties.getPropertyKeys() == ["key"] as Set + + assert emptyProperties.size() == 0 + assert emptyProperties.getPropertyKeys() == [] as Set + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf new file mode 100644 index 0000000..9225126 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf @@ -0,0 +1,74 @@ +# +# 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. +# + +# Java command to use when running NiFi +java=java + +# Username to use when running NiFi. This value will be ignored on Windows. +run.as= + +# Configure where NiFi's lib and conf directories live +lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling NiFi to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true + +# JVM memory settings +java.arg.2=-Xms512m +java.arg.3=-Xmx512m + +# Enable Remote Debugging +#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 + +java.arg.4=-Djava.net.preferIPv4Stack=true + +# allowRestrictedHeaders is required for Cluster/Node communications to work properly +java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true +java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol + +# The G1GC is still considered experimental but has proven to be very advantageous in providing great +# performance without significant "stop-the-world" delays. +java.arg.13=-XX:+UseG1GC + +#Set headless mode by default +java.arg.14=-Djava.awt.headless=true + +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 + +### +# Notification Services for notifying interested parties when NiFi is stopped, started, dies +### + +# XML File that contains the definitions of the notification services +notification.services.file=./conf/bootstrap-notification-services.xml + +# In the case that we are unable to send a notification for an event, how many times should we retry? +notification.max.attempts=5 + +# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi is started? +#nifi.start.notification.services=email-notification + +# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi is stopped? +#nifi.stop.notification.services=email-notification + +# Comma-separated list of identifiers that are present in the notification.services.file; which services should be used to notify when NiFi dies? +#nifi.dead.notification.services=email-notification \ No newline at end of file
