NIFI-6999 - Made changes to load flow.xml files using streams. Updated tests.

NIFI-6999 - Slight change to test to check for WARN message.

NIFI-6999 - Removed very large flow file and test that uses it. This test ran for about 2 minutes so was excessive to keep in. The other changed tests to handle streams proves the functionality. A large file can be used on the command line to manually test large flow files. Some other cleanup.

NIFI-6999 - Removed comments and altered the code a little bit for readability as per code review.

NIFI-6999 - Removed commented code

NIFI-6999 - Renamed variable and removed assert comment.

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #4715.
This commit is contained in:
Nathan Gough 2020-12-04 19:05:52 -05:00
parent f2a16cd02e
commit 1c361d45ae
6 changed files with 320 additions and 167 deletions

View File

@ -27,7 +27,6 @@ import org.apache.commons.cli.Option
import org.apache.commons.cli.Options
import org.apache.commons.cli.ParseException
import org.apache.commons.codec.binary.Hex
import org.apache.commons.io.IOUtils
import org.apache.nifi.security.kms.CryptoUtils
import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
import org.apache.nifi.toolkit.tls.commandLine.ExitCode
@ -47,12 +46,17 @@ import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.PBEParameterSpec
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption;
import java.security.KeyException
import java.security.SecureRandom
import java.security.Security
import java.util.regex.Matcher
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipException
import java.nio.file.Files;
class ConfigEncryptionTool {
private static final Logger logger = LoggerFactory.getLogger(ConfigEncryptionTool.class)
@ -64,8 +68,8 @@ class ConfigEncryptionTool {
public String outputLoginIdentityProvidersPath
public String authorizersPath
public String outputAuthorizersPath
public String flowXmlPath
public String outputFlowXmlPath
public static flowXmlPath
public outputFlowXmlPath
private String keyHex
private String migrationKeyHex
@ -81,7 +85,7 @@ class ConfigEncryptionTool {
private NiFiProperties niFiProperties
private String loginIdentityProviders
private String authorizers
private String flowXml
private InputStream flowXmlInputStream
private boolean usingPassword = true
private boolean usingPasswordMigration = true
@ -657,26 +661,24 @@ class ConfigEncryptionTool {
/**
* Loads the flow definition from the provided file path, handling the GZIP file compression. Unlike {@link #loadLoginIdentityProviders()} this method does not decrypt the content (for performance and separation of concern reasons).
*
* @return the file content
* @param The path to the XML file
* @return The file content
* @throw IOException if the flow.xml.gz file cannot be read
*/
private String loadFlowXml() throws IOException {
if (flowXmlPath && (new File(flowXmlPath)).exists()) {
private InputStream loadFlowXml(String filePath) throws IOException {
if (filePath && (new File(filePath)).exists()) {
try {
new FileInputStream(flowXmlPath).withCloseable {
new GZIPInputStream(it).withCloseable {
String xmlContent = IOUtils.toString(it, StandardCharsets.UTF_8)
return xmlContent
}
}
return new GZIPInputStream(new FileInputStream(filePath))
} catch (ZipException e) {
return new FileInputStream(filePath)
} catch (RuntimeException e) {
if (isVerbose) {
logger.error("Encountered an error", e)
}
throw new IOException("Cannot load flow from [${flowXmlPath}]", e)
throw new IOException("Cannot load flow from [${filePath}]", e)
}
} else {
printUsageAndThrow("Cannot load flow from [${flowXmlPath}]", ExitCode.ERROR_READING_NIFI_PROPERTIES)
printUsageAndThrow("Cannot load flow from [${filePath}]", ExitCode.ERROR_READING_NIFI_PROPERTIES)
}
}
@ -790,14 +792,14 @@ class ConfigEncryptionTool {
/**
* Scans XML content and decrypts each encrypted element, then re-encrypts it with the new key, and returns the final XML content.
*
* @param flowXmlContent the original flow.xml.gz content
* @param flowXmlContent the original flow.xml.gz as an input stream
* @param existingFlowPassword the existing value of nifi.sensitive.props.key (not a raw key, but rather a password)
* @param newFlowPassword the password to use to for encryption (not a raw key, but rather a password)
* @param existingAlgorithm the KDF algorithm to use (defaults to PBEWITHMD5AND256BITAES-CBC-OPENSSL)
* @param existingProvider the {@link java.security.Provider} to use (defaults to BC)
* @return the encrypted XML content
* @return the encrypted XML content as an InputStream
*/
private String migrateFlowXmlContent(String flowXmlContent, String existingFlowPassword, String newFlowPassword, String existingAlgorithm = DEFAULT_FLOW_ALGORITHM, String existingProvider = DEFAULT_PROVIDER, String newAlgorithm = DEFAULT_FLOW_ALGORITHM, String newProvider = DEFAULT_PROVIDER) {
private InputStream migrateFlowXmlContent(InputStream flowXmlContent, String existingFlowPassword, String newFlowPassword, String existingAlgorithm = DEFAULT_FLOW_ALGORITHM, String existingProvider = DEFAULT_PROVIDER, String newAlgorithm = DEFAULT_FLOW_ALGORITHM, String newProvider = DEFAULT_PROVIDER) {
/* For re-encryption, for performance reasons, we will use a fixed salt for all of
* the operations. These values are stored in the same file and the default key is in the
* source code (see NIFI-1465 and NIFI-1277), so the security trade-off is minimal
@ -810,21 +812,51 @@ class ConfigEncryptionTool {
Cipher encryptCipher = generateFlowEncryptionCipher(newFlowPassword, encryptionSalt, newAlgorithm, newProvider)
int elementCount = 0
File tempFlowXmlFile = new File(getTemporaryFlowXmlFile(outputFlowXmlPath).toString())
BufferedWriter tempFlowXmlWriter = getFlowOutputStream(tempFlowXmlFile, flowXmlContent instanceof GZIPInputStream)
// Scan the XML content and identify every encrypted element, decrypt it, and replace it with the re-encrypted value
String migratedFlowXmlContent = flowXmlContent.replaceAll(WRAPPED_FLOW_XML_CIPHER_TEXT_REGEX) { String wrappedCipherText ->
String plaintext = decryptFlowElement(wrappedCipherText, existingFlowPassword, existingAlgorithm, existingProvider)
byte[] cipherBytes = encryptCipher.doFinal(plaintext.bytes)
byte[] saltAndCipherBytes = concatByteArrays(encryptionSalt, cipherBytes)
elementCount++
"enc{${Hex.encodeHex(saltAndCipherBytes)}}"
// Scan through XML content as a stream, decrypt and re-encrypt fields with a new flow password
final BufferedReader reader = new BufferedReader(new InputStreamReader(flowXmlContent))
String line;
while((line = reader.readLine()) != null) {
def matcher = line =~ WRAPPED_FLOW_XML_CIPHER_TEXT_REGEX
if(matcher.find()) {
String plaintext = decryptFlowElement(matcher.getAt(0), existingFlowPassword, existingAlgorithm, existingProvider)
byte[] cipherBytes = encryptCipher.doFinal(plaintext.bytes)
byte[] saltAndCipherBytes = concatByteArrays(encryptionSalt, cipherBytes)
elementCount++
tempFlowXmlWriter.writeLine(line.replaceFirst(WRAPPED_FLOW_XML_CIPHER_TEXT_REGEX, "enc{${Hex.encodeHex(saltAndCipherBytes)}}"))
} else {
tempFlowXmlWriter.writeLine(line)
}
}
tempFlowXmlWriter.flush()
tempFlowXmlWriter.close()
// Overwrite the original flow file with the migrated flow file
Files.move(tempFlowXmlFile.toPath(), Paths.get(outputFlowXmlPath), StandardCopyOption.ATOMIC_MOVE)
if (isVerbose) {
logger.info("Decrypted and re-encrypted ${elementCount} elements for flow.xml.gz")
}
migratedFlowXmlContent
loadFlowXml(outputFlowXmlPath)
}
private BufferedWriter getFlowOutputStream(File outputFlowXmlPath, boolean isFileGZipped) {
OutputStream flowOutputStream = new FileOutputStream(outputFlowXmlPath)
if(isFileGZipped) {
flowOutputStream = new GZIPOutputStream(flowOutputStream)
}
new BufferedWriter(new OutputStreamWriter(flowOutputStream))
}
// Create a temporary output file we can write the stream to
private Path getTemporaryFlowXmlFile(String originalOutputFlowXmlPath) {
String outputFilename = Paths.get(originalOutputFlowXmlPath).getFileName().toString()
String migratedFileName = "migrated-${outputFilename}"
Paths.get(originalOutputFlowXmlPath).resolveSibling(migratedFileName)
}
/**
@ -861,19 +893,6 @@ class ConfigEncryptionTool {
encryptCipher
}
/**
* Writes the XML content to the {@link .outputFlowXmlPath} location, handling the GZIP file compression.
*
* @param flowXmlContent the XML content to write
*/
private void writeFlowXmlToFile(String flowXmlContent) {
new FileOutputStream(outputFlowXmlPath).withCloseable {
new GZIPOutputStream(it).withCloseable {
IOUtils.write(flowXmlContent, it, StandardCharsets.UTF_8)
}
}
}
String decryptLoginIdentityProviders(String encryptedXml, String existingKeyHex = keyHex) {
AESSensitivePropertyProvider sensitivePropertyProvider = new AESSensitivePropertyProvider(existingKeyHex)
@ -1536,14 +1555,13 @@ class ConfigEncryptionTool {
if (tool.handlingFlowXml) {
try {
tool.flowXml = tool.loadFlowXml()
tool.flowXmlInputStream = tool.loadFlowXml(flowXmlPath)
} catch (Exception e) {
if (tool.isVerbose) {
logger.error("Encountered an error: ", e)
}
tool.printUsageAndThrow("Cannot load flow.xml.gz", ExitCode.ERROR_READING_NIFI_PROPERTIES)
}
tool.handleFlowXml(existingNiFiPropertiesAreEncrypted)
}
if (tool.handlingNiFiProperties) {
@ -1568,7 +1586,7 @@ class ConfigEncryptionTool {
tool.writeKeyToBootstrapConf()
}
if (tool.handlingFlowXml) {
tool.writeFlowXmlToFile(tool.flowXml)
tool.handleFlowXml(tool.niFiPropertiesAreEncrypted())
}
if (tool.handlingNiFiProperties || tool.handlingFlowXml) {
tool.writeNiFiProperties()
@ -1612,7 +1630,8 @@ class ConfigEncryptionTool {
String newProvider = newFlowProvider ?: existingProvider
try {
flowXml = migrateFlowXmlContent(flowXml, existingFlowPassword, newFlowPassword, existingAlgorithm, existingProvider, newAlgorithm, newProvider)
logger.info("Migrating flow.xml file at ${flowXmlPath}. This could take a while if the flow XML is very large.")
migrateFlowXmlContent(flowXmlInputStream, existingFlowPassword, newFlowPassword, existingAlgorithm, existingProvider, newAlgorithm, newProvider)
} catch (Exception e) {
logger.error("Encountered an error: ${e.getLocalizedMessage()}")
if (e instanceof BadPaddingException) {

View File

@ -60,6 +60,9 @@ import java.nio.file.attribute.PosixFilePermission
import java.security.KeyException
import java.security.Security
import org.apache.commons.io.IOUtils
import java.nio.charset.StandardCharsets
@RunWith(JUnit4.class)
class ConfigEncryptionToolTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(ConfigEncryptionToolTest.class)
@ -77,6 +80,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
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 PASSWORD = "thisIsABadPassword"
private static final String ANOTHER_PASSWORD = "thisIsAnotherBadPassword"
private static final String STATIC_SALT = "\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUV"
private static final String SCRYPT_SALT_PATTERN = /\$\w{2}\$\w{5,}\$[\w\/\=\+]+/
@ -287,7 +291,9 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
// Assert
assert !TestAppender.events.isEmpty()
assert TestAppender.events.first().message =~ "The source nifi.properties and destination nifi.properties are identical \\[.*\\] so the original will be overwritten"
assert TestAppender.events.stream().any() {
it.message =~ "The source nifi.properties and destination nifi.properties are identical \\[.*\\] so the original will be overwritten"
}
}
@Test
@ -3389,7 +3395,6 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
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.userGroupProvider.find {
it.identifier == 'ldap-user-group-provider'
@ -3806,19 +3811,21 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
// Verify the flow definition
def verifyTool = new ConfigEncryptionTool()
verifyTool.isVerbose = true
verifyTool.flowXmlPath = workingFlowXmlFile.path
String updatedFlowXmlContent = verifyTool.loadFlowXml()
InputStream updatedFlowXmlContent = verifyTool.loadFlowXml(workingFlowXmlFile.path)
// Check that the flow.xml.gz content changed
assert updatedFlowXmlContent != ORIGINAL_FLOW_XML_CONTENT
// Verify that the cipher texts decrypt correctly
logger.info("Original flow.xml.gz cipher texts: ${originalFlowCipherTexts}")
def flowCipherTexts = updatedFlowXmlContent.findAll(WFXCTR)
logger.info("Updated flow.xml.gz cipher texts: ${flowCipherTexts}")
assert flowCipherTexts.size() == CIPHER_TEXT_COUNT
flowCipherTexts.every {
assert ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword) == "thisIsABadPassword"
def updatedFlowCipherTexts = findFieldsInStream(updatedFlowXmlContent, WFXCTR)
logger.info("Updated flow.xml.gz cipher texts: ${updatedFlowCipherTexts}")
assert updatedFlowCipherTexts.size() == CIPHER_TEXT_COUNT
updatedFlowCipherTexts.each {
String decryptedValue = ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword)
logger.info("Decrypted value of migrated ${workingFlowXmlFile.path} was: ${decryptedValue}")
assert decryptedValue == PASSWORD || decryptedValue == ANOTHER_PASSWORD
}
}
})
@ -3911,19 +3918,20 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
// Verify the flow definition
def verifyTool = new ConfigEncryptionTool()
verifyTool.isVerbose = true
verifyTool.flowXmlPath = workingFlowXmlFile.path
String updatedFlowXmlContent = verifyTool.loadFlowXml()
InputStream migratedFlowXmlContent = verifyTool.loadFlowXml(workingFlowXmlFile.path)
// Check that the flow.xml.gz cipher texts did change (new salt)
assert updatedFlowXmlContent != ORIGINAL_FLOW_XML_CONTENT
assert migratedFlowXmlContent != ORIGINAL_FLOW_XML_CONTENT
// Verify that the cipher texts decrypt correctly
logger.info("Original flow.xml.gz cipher texts: ${originalFlowCipherTexts}")
def flowCipherTexts = updatedFlowXmlContent.findAll(WFXCTR)
logger.info("Updated flow.xml.gz cipher texts: ${flowCipherTexts}")
assert flowCipherTexts.size() == CIPHER_TEXT_COUNT
flowCipherTexts.every {
assert ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword) == "thisIsABadPassword"
def migratedFlowCipherTexts = findFieldsInStream(migratedFlowXmlContent, WFXCTR)
logger.info("Updated flow.xml.gz cipher texts: ${migratedFlowCipherTexts}")
assert migratedFlowCipherTexts.size() == CIPHER_TEXT_COUNT
migratedFlowCipherTexts.each {
String decryptedValue = ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword)
logger.info("Decrypted value of migrated ${workingFlowXmlFile.path} was: ${decryptedValue}")
assert decryptedValue == PASSWORD || decryptedValue == ANOTHER_PASSWORD
}
}
})
@ -3969,7 +3977,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
Files.copy(niFiPropertiesFile.toPath(), workingNiFiPropertiesFile.toPath())
// Use a flow definition that was encrypted with the hard-coded default SP key
File flowXmlFile = new File("src/test/resources/flow_default_key.xml.gz")
File flowXmlFile = new File("src/test/resources/flow.xml.gz")
File workingFlowXmlFile = new File("target/tmp/tmp-flow.xml.gz")
workingFlowXmlFile.delete()
Files.copy(flowXmlFile.toPath(), workingFlowXmlFile.toPath())
@ -4053,18 +4061,154 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
def verifyTool = new ConfigEncryptionTool()
verifyTool.isVerbose = true
verifyTool.flowXmlPath = workingFlowXmlFile.path
String updatedFlowXmlContent = verifyTool.loadFlowXml()
InputStream updatedFlowXmlContent = verifyTool.loadFlowXml(workingFlowXmlFile.path)
// Check that the flow.xml.gz content changed
assert updatedFlowXmlContent != ORIGINAL_FLOW_XML_CONTENT
def migratedFlowCipherTexts = findFieldsInStream(updatedFlowXmlContent, WFXCTR)
// Verify that the cipher texts decrypt correctly
logger.info("Original flow.xml.gz cipher texts: ${originalFlowCipherTexts}")
def flowCipherTexts = updatedFlowXmlContent.findAll(WFXCTR)
logger.info("Updated flow.xml.gz cipher texts: ${flowCipherTexts}")
assert flowCipherTexts.size() == CIPHER_TEXT_COUNT
flowCipherTexts.every {
assert ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword) == "thisIsABadPassword"
logger.info("Updated flow.xml.gz cipher texts: ${migratedFlowCipherTexts}")
assert migratedFlowCipherTexts.size() == CIPHER_TEXT_COUNT
migratedFlowCipherTexts.each {
String decryptedValue = ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword)
logger.info("Decrypted value of migrated ${workingFlowXmlFile.path} was: ${decryptedValue}")
assert decryptedValue == PASSWORD || decryptedValue == ANOTHER_PASSWORD
}
}
})
// Act
ConfigEncryptionTool.main(args)
logger.info("Invoked #main with ${args.join(" ")}")
}
/**
* In this scenario, the nifi.properties file has a sensitive key value which is already encrypted. The goal is to provide a new provide a new sensitive key value, perform the migration of the flow.xml.gz, and update nifi.properties with a new encrypted sensitive key value without modifying any other nifi.properties values.
*/
@Test
void testShouldPerformFullOperationOnAFlowXmlWithPreviouslyEncryptedNiFiProperties() {
// Arrange
exit.expectSystemExitWithStatus(0)
File tmpDir = setupTmpDir()
File passwordKeyFile = new File("src/test/resources/bootstrap_with_root_key_password_128.conf")
File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
bootstrapFile.delete()
Files.copy(passwordKeyFile.toPath(), bootstrapFile.toPath())
final List<String> originalBootstrapLines = bootstrapFile.readLines()
String originalKeyLine = originalBootstrapLines.find {
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
}
final String EXPECTED_KEY_LINE = ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + PASSWORD_KEY_HEX_128
logger.info("Original key line from bootstrap.conf: ${originalKeyLine}")
assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + PASSWORD_KEY_HEX_128
// Not "handling" NFP, so update in place (not source test resource)
String niFiPropertiesTemplatePath = "src/test/resources/nifi_with_few_sensitive_properties_protected_aes_password_128.properties"
File niFiPropertiesFile = new File(niFiPropertiesTemplatePath)
File workingNiFiPropertiesFile = new File("target/tmp/tmp-nifi.properties")
workingNiFiPropertiesFile.delete()
Files.copy(niFiPropertiesFile.toPath(), workingNiFiPropertiesFile.toPath())
// Use a flow definition that was encrypted with the hard-coded default SP key
File flowXmlFile = new File("src/test/resources/flow.xml.gz")
File workingFlowXmlFile = new File("target/tmp/tmp-flow.xml.gz")
workingFlowXmlFile.delete()
Files.copy(flowXmlFile.toPath(), workingFlowXmlFile.toPath())
// Get the original ciphered fields to compare later
def verifyTool = new ConfigEncryptionTool()
verifyTool.isVerbose = true
def originalFlowCipherTexts = findFieldsInStream(verifyTool.loadFlowXml(flowXmlFile.path), WFXCTR)
final int CIPHER_TEXT_COUNT = originalFlowCipherTexts.size()
// Load both the encrypted and decrypted properties to compare later
NiFiPropertiesLoader niFiPropertiesLoader = NiFiPropertiesLoader.withKey(PASSWORD_KEY_HEX_128)
NiFiProperties inputProperties = niFiPropertiesLoader.load(workingNiFiPropertiesFile)
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}")
final String SENSITIVE_PROTECTION_KEY = ProtectedNiFiProperties.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY)
ProtectedNiFiProperties encryptedProperties = niFiPropertiesLoader.readProtectedPropertiesFromDisk(workingNiFiPropertiesFile)
def originalEncryptedValues = encryptedProperties.getSensitivePropertyKeys().collectEntries { String key -> [(key): encryptedProperties.getProperty(key)] }
logger.info("Original encrypted values: ${originalEncryptedValues}")
String originalSensitiveKeyProtectionScheme = encryptedProperties.getProperty(SENSITIVE_PROTECTION_KEY)
logger.info("Sensitive property key originally protected with ${originalSensitiveKeyProtectionScheme}")
String newFlowPassword = FLOW_PASSWORD
// Bootstrap path must be provided to decrypt nifi.properties to get SP key
String[] args = ["-n", workingNiFiPropertiesFile.path, "-f", workingFlowXmlFile.path, "-b", bootstrapFile.path, "-x", "-v", "-s", newFlowPassword]
exit.checkAssertionAfterwards(new Assertion() {
void checkAssertion() {
final List<String> updatedPropertiesLines = workingNiFiPropertiesFile.readLines()
logger.info("Updated nifi.properties:")
logger.info("\n" * 2 + updatedPropertiesLines.join("\n"))
AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(PASSWORD_KEY_HEX_128)
// Check that the output values for everything is the same except the sensitive props key
NiFiProperties updatedProperties = new NiFiPropertiesLoader().readProtectedPropertiesFromDisk(workingNiFiPropertiesFile)
assert updatedProperties.size() == inputProperties.size()
String newSensitivePropertyKey = updatedProperties.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY)
// Check that the encrypted value changed
assert newSensitivePropertyKey != originalSensitiveValues.get(NiFiProperties.SENSITIVE_PROPS_KEY)
// Check that the decrypted value is the new password
assert spp.unprotect(newSensitivePropertyKey) == newFlowPassword
// Check that all other values stayed the same
originalEncryptedValues.every { String key, String originalValue ->
if (key != NiFiProperties.SENSITIVE_PROPS_KEY) {
assert updatedProperties.getProperty(key) == originalValue
}
}
// Check that all other (decrypted) values stayed the same
originalSensitiveValues.every { String key, String originalValue ->
if (key != NiFiProperties.SENSITIVE_PROPS_KEY) {
assert spp.unprotect(updatedProperties.getProperty(key)) == originalValue
}
}
// Check that the protection scheme did not change
String sensitiveKeyProtectionScheme = updatedProperties.getProperty(SENSITIVE_PROTECTION_KEY)
logger.info("Sensitive property key currently protected with ${sensitiveKeyProtectionScheme}")
assert sensitiveKeyProtectionScheme == originalSensitiveKeyProtectionScheme
// Check that bootstrap.conf did not change
final List<String> 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()
// Verify the flow definition
verifyTool = new ConfigEncryptionTool()
verifyTool.isVerbose = true
InputStream migratedFlowXmlContent = verifyTool.loadFlowXml(workingFlowXmlFile.path)
def migratedFlowCipherTexts = findFieldsInStream(migratedFlowXmlContent, WFXCTR)
logger.info("Migrated flow cipher texts for: " + workingFlowXmlFile.path)
// Verify that the cipher texts decrypt correctly
logger.info("Original " + workingFlowXmlFile.path + " unique cipher texts: ${originalFlowCipherTexts}")
logger.info("Migrated " + workingFlowXmlFile.path + " unique cipher texts: ${migratedFlowCipherTexts}")
assert migratedFlowCipherTexts.size() == CIPHER_TEXT_COUNT
migratedFlowCipherTexts.each {
String decryptedValue = ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword)
logger.info("Decrypted value of migrated ${workingFlowXmlFile.path} was: ${decryptedValue}")
assert decryptedValue == PASSWORD || decryptedValue == ANOTHER_PASSWORD
}
}
})
@ -4118,7 +4262,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
// Read the uncompressed version to compare later
File originalFlowXmlFile = new File("src/test/resources/flow_default_key.xml")
final String ORIGINAL_FLOW_XML_CONTENT = originalFlowXmlFile.text
def originalFlowCipherTexts = ORIGINAL_FLOW_XML_CONTENT.findAll(WFXCTR)
def originalFlowCipherTexts = ORIGINAL_FLOW_XML_CONTENT.findAll(WFXCTR).toSet()
final int CIPHER_TEXT_COUNT = originalFlowCipherTexts.size()
// Load both the encrypted and decrypted properties to compare later
@ -4209,19 +4353,20 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
// Verify the flow definition
def verifyTool = new ConfigEncryptionTool()
verifyTool.isVerbose = true
verifyTool.flowXmlPath = workingFlowXmlFile.path
String updatedFlowXmlContent = verifyTool.loadFlowXml()
InputStream updatedFlowXmlContent = verifyTool.loadFlowXml(workingFlowXmlFile.path)
// Check that the flow.xml.gz content changed
assert updatedFlowXmlContent != ORIGINAL_FLOW_XML_CONTENT
// TODO assert updatedFlowXmlContent != ORIGINAL_FLOW_XML_CONTENT
// Verify that the cipher texts decrypt correctly
logger.info("Original flow.xml.gz cipher texts: ${originalFlowCipherTexts}")
def flowCipherTexts = updatedFlowXmlContent.findAll(WFXCTR)
def flowCipherTexts = findFieldsInStream(updatedFlowXmlContent, WFXCTR)
logger.info("Updated flow.xml.gz cipher texts: ${flowCipherTexts}")
assert flowCipherTexts.size() == CIPHER_TEXT_COUNT
flowCipherTexts.every {
assert ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword) == "thisIsABadPassword"
flowCipherTexts.each {
String decryptedValue = ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword)
logger.info("Decrypted value of migrated ${workingFlowXmlFile.path} was: ${decryptedValue}")
assert decryptedValue == PASSWORD || decryptedValue == ANOTHER_PASSWORD
}
// Update the "original" flow cipher texts for the next run to the current values
@ -4289,7 +4434,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
@Test
void testShouldDecryptFlowXmlContent() {
// Arrange
String existingFlowPassword = "flowPassword"
String existingFlowPassword = "nififtw!"
final String DEFAULT_ALGORITHM = "PBEWITHMD5AND256BITAES-CBC-OPENSSL"
final String DEFAULT_PROVIDER = "BC"
@ -4326,7 +4471,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
final String EXPECTED_PLAINTEXT = "thisIsABadPassword"
final String ENCRYPTED_VALUE_FROM_FLOW = "enc{2032416987A00D9FCD757528D7AE609D7E793CA5F956641DB53E14CDB9BFCD4037B73AC705CD3F5C1C1BDE18B8D7B281}"
final String ENCRYPTED_VALUE_FROM_FLOW = "enc{5d8c45f04790e73cba72e5e3fbee1145f2e18256c3b33c283e17f5281611cb5e5f9e6cc988c5be0e8cca7b5dc8fa7cf7}"
// Act
String decryptedElement = ConfigEncryptionTool.decryptFlowElement(ENCRYPTED_VALUE_FROM_FLOW, existingFlowPassword, DEFAULT_ALGORITHM, DEFAULT_PROVIDER)
@ -4339,8 +4484,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
@Test
void testShouldEncryptFlowXmlContent() {
// Arrange
String flowPassword = "flowPassword"
String sensitivePropertyValue = "thisIsABadProcessorPassword"
String flowPassword = "nififtw!"
String sensitivePropertyValue = "thisIsAnotherBadPassword"
byte[] saltBytes = "thisIsABadSalt..".bytes
StringEncryptor sanityEncryptor = new StringEncryptor(DEFAULT_ALGORITHM, DEFAULT_PROVIDER, flowPassword)
@ -4405,34 +4550,40 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
Files.copy(flowXmlFile.toPath(), workingFile.toPath())
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.isVerbose = true
tool.flowXmlPath = workingFile.path
tool.outputFlowXmlPath = workingFile.path
final String SENSITIVE_VALUE = "thisIsABadPassword"
String existingFlowPassword = DEFAULT_LEGACY_SENSITIVE_PROPS_KEY
String newFlowPassword = FLOW_PASSWORD
String xmlContent = workingFile.text
logger.info("Read flow.xml: \n${xmlContent}")
InputStream xmlContent = new FileInputStream(workingFile.path)
logger.info("Read flow.xml as input stream.")
// There are two encrypted passwords in this flow
int cipherTextCount = xmlContent.findAll(WFXCTR).size()
logger.info("Found ${cipherTextCount} encrypted properties in the original flow.xml content")
int cipherTextCount = findFieldsInStream(xmlContent, WFXCTR).size()
logger.info("Found ${cipherTextCount} unique encrypted properties in the original flow.xml content")
// Act
String migratedXmlContent = tool.migrateFlowXmlContent(xmlContent, existingFlowPassword, newFlowPassword)
logger.info("Migrated flow.xml: \n${migratedXmlContent}")
xmlContent = new FileInputStream(workingFile.path)
tool.migrateFlowXmlContent(xmlContent, existingFlowPassword, newFlowPassword)
logger.info("Migrated flow.xml.")
// Assert
def newCipherTexts = migratedXmlContent.findAll(WFXCTR)
InputStream migratedFlowXmlFile = new FileInputStream(workingFile.path)
def migratedCipherTexts = findFieldsInStream(migratedFlowXmlFile, WFXCTR)
assert newCipherTexts.size() == cipherTextCount
newCipherTexts.every {
assert ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword) == SENSITIVE_VALUE
assert migratedCipherTexts.size() == cipherTextCount
migratedCipherTexts.each {
String decryptedValue = ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword)
logger.info("Decrypted value of migrated " + workingFile.path + " was: " + decryptedValue)
assert decryptedValue == PASSWORD || decryptedValue == ANOTHER_PASSWORD
}
// Ensure that everything else is identical
assert migratedXmlContent.replaceAll(WFXCTR, "") ==
xmlContent.replaceAll(WFXCTR, "")
assert flowXmlFile.text.replaceAll(WFXCTR, "") ==
workingFile.text.replaceAll(WFXCTR, "")
}
@ -4449,6 +4600,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
Files.copy(flowXmlFile.toPath(), workingFile.toPath())
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.isVerbose = true
tool.outputFlowXmlPath = workingFile.path
final String SENSITIVE_VALUE = "thisIsABadPassword"
@ -4464,7 +4616,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
final int ORIGINAL_CIPHER_TEXT_COUNT = ORIGINAL_CIPHER_TEXTS.size()
logger.info("Found ${ORIGINAL_CIPHER_TEXT_COUNT} encrypted properties in the original flow.xml content")
String currentXmlContent = xmlContent
InputStream currentXmlContent = new ByteArrayInputStream(xmlContent.bytes)
// Act
passwordProgression.eachWithIndex { String existingFlowPassword, int i ->
@ -4472,24 +4624,26 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
String newFlowPassword = passwordProgression[i + 1]
logger.info("Migrating from ${existingFlowPassword} to ${newFlowPassword}")
String migratedXmlContent = tool.migrateFlowXmlContent(currentXmlContent, existingFlowPassword, newFlowPassword)
InputStream migratedXmlContent = tool.migrateFlowXmlContent(currentXmlContent, existingFlowPassword, newFlowPassword)
// logger.info("Migrated flow.xml: \n${migratedXmlContent}")
// Assert
def newCipherTexts = migratedXmlContent.findAll(WFXCTR)
def newCipherTexts = findFieldsInStream(migratedXmlContent, WFXCTR)
logger.info("Cipher texts for iteration ${i}: \n${newCipherTexts.join("\n")}")
assert newCipherTexts.size() == ORIGINAL_CIPHER_TEXT_COUNT
newCipherTexts.every {
assert ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword) == SENSITIVE_VALUE
newCipherTexts.each {
String decryptedValue = ConfigEncryptionTool.decryptFlowElement(it, newFlowPassword)
assert decryptedValue == PASSWORD || decryptedValue == ANOTHER_PASSWORD
}
// Ensure that everything else is identical
assert migratedXmlContent.replaceAll(WFXCTR, "") ==
xmlContent.replaceAll(WFXCTR, "")
assert new File(workingFile.path).text.replaceAll(WFXCTR, "") ==
flowXmlFile.text.replaceAll(WFXCTR, "")
// Update the "source" XML content for the next iteration
currentXmlContent = migratedXmlContent
currentXmlContent = tool.loadFlowXml(workingFile.path)
}
}
}
@ -4507,6 +4661,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
Files.copy(flowXmlFile.toPath(), workingFile.toPath())
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.isVerbose = true
tool.outputFlowXmlPath = workingFile.path
String existingFlowPassword = DEFAULT_LEGACY_SENSITIVE_PROPS_KEY
String newFlowPassword = FLOW_PASSWORD
@ -4519,11 +4674,11 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
logger.info("Found ${cipherTextCount} encrypted properties in the original flow.xml content")
// Act
String migratedXmlContent = tool.migrateFlowXmlContent(xmlContent, existingFlowPassword, newFlowPassword)
logger.info("Migrated flow.xml: \n${migratedXmlContent}")
InputStream migratedXmlContent = tool.migrateFlowXmlContent(new ByteArrayInputStream(xmlContent.bytes), existingFlowPassword, newFlowPassword)
logger.info("Migrated flow.xml.")
// Assert
def newCipherTexts = migratedXmlContent.findAll(WFXCTR)
def newCipherTexts = findFieldsInStream(migratedXmlContent, WFXCTR)
assert newCipherTexts.size() == cipherTextCount
@ -4551,17 +4706,19 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
Files.copy(flowXmlFile.toPath(), workingFile.toPath())
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.isVerbose = true
tool.flowXmlPath = workingFile.path
tool.outputFlowXmlPath = workingFile.path
// Use the wrong existing password
String wrongExistingFlowPassword = DEFAULT_LEGACY_SENSITIVE_PROPS_KEY.reverse()
String newFlowPassword = FLOW_PASSWORD
String xmlContent = workingFile.text
InputStream xmlContent = new ByteArrayInputStream(workingFile.bytes)
// Act
def message = shouldFail(BadPaddingException) {
String migratedXmlContent = tool.migrateFlowXmlContent(xmlContent, wrongExistingFlowPassword, newFlowPassword)
logger.info("Migrated flow.xml: \n${migratedXmlContent}")
InputStream migratedXmlContent = tool.migrateFlowXmlContent(xmlContent, wrongExistingFlowPassword, newFlowPassword)
logger.info("Migrated flow.xml.")
}
logger.expected(message)
@ -4575,26 +4732,23 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
@Test
void testHandleFlowXmlMigrationWithIncorrectExistingPasswordShouldProvideHelpfulErrorMessage() {
// Arrange
// exit.expectSystemExitWithStatus(ExitCode.ERROR_MIGRATING_FLOW.ordinal())
systemOutRule.clearLog()
String flowXmlPath = "src/test/resources/flow.xml"
File flowXmlFile = new File(flowXmlPath)
File tmpDir = setupTmpDir()
// Use the wrong existing password
String wrongExistingFlowPassword = DEFAULT_LEGACY_SENSITIVE_PROPS_KEY.reverse()
String newFlowPassword = FLOW_PASSWORD
def nifiProperties = wrapNFP([(NiFiProperties.SENSITIVE_PROPS_KEY): wrongExistingFlowPassword])
File workingFile = new File("target/tmp/tmp-flow.xml")
workingFile.delete()
Files.copy(flowXmlFile.toPath(), workingFile.toPath())
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.isVerbose = true
// Use the wrong existing password
String wrongExistingFlowPassword = DEFAULT_LEGACY_SENSITIVE_PROPS_KEY.reverse()
String newFlowPassword = FLOW_PASSWORD
tool.flowXml = workingFile.text
def nifiProperties = wrapNFP([(NiFiProperties.SENSITIVE_PROPS_KEY): wrongExistingFlowPassword])
tool.flowXmlInputStream = tool.loadFlowXml(workingFile.path)
tool.niFiProperties = nifiProperties
tool.flowPropertiesPassword = newFlowPassword
tool.handlingNiFiProperties = false
@ -4602,23 +4756,12 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
// Act
def message = shouldFail(Exception) {
tool.handleFlowXml()
logger.info("Migrated flow.xml: \n${tool.flowXml}")
logger.info("Migrated flow.xml.")
}
logger.expected(message)
// final String standardOutput = systemOutRule.getLog()
// List<String> lines = standardOutput.split("\n")
// logger.info("Captured ${lines.size()} lines of log output")
// lines.each { String l -> logger.info("\t$l") }
// final String errorOutput = systemErrRule.getLog()
// List<String> errorlines = errorOutput.split("\n")
// logger.info("Captured ${errorlines.size()} lines of error log output")
// errorlines.each { String l -> logger.info("\t$l") }
// Assert
// TODO: Assert that this message was in the log output (neither the STDOUT and STDERR buffers contain it, but it is printed)
// assert message =~ "Error performing flow XML content migration because some sensitive values could not be decrypted. Ensure that the existing flow password \\[\\-p\\] is correct."
assert message == "Encountered an error migrating flow content"
}
@ -4646,53 +4789,20 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
Files.copy(flowXmlGzFile.toPath(), workingGzFile.toPath())
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.isVerbose = true
tool.flowXmlPath = workingGzFile.path
String xmlContent = workingFile.text
logger.info("Read flow.xml: \n${xmlContent}")
// Act
String readXmlContent = tool.loadFlowXml()
InputStream xmlContentStream = tool.loadFlowXml(workingGzFile.path)
String readXmlContent = IOUtils.toString(xmlContentStream, StandardCharsets.UTF_8)
logger.info("Loaded flow.xml.gz: \n${readXmlContent}")
// Assert
assert readXmlContent == xmlContent
}
@Test
void testShouldWriteFlowXmlToFile() {
// Arrange
String flowXmlPath = "src/test/resources/flow.xml"
File flowXmlFile = new File(flowXmlPath)
String flowXmlGzPath = "src/test/resources/flow.xml.gz"
File flowXmlGzFile = new File(flowXmlGzPath)
File tmpDir = setupTmpDir()
File workingFile = new File("target/tmp/tmp-flow.xml")
workingFile.delete()
Files.copy(flowXmlFile.toPath(), workingFile.toPath())
File workingGzFile = new File("target/tmp/tmp-flow.xml.gz")
workingGzFile.delete()
Files.copy(flowXmlGzFile.toPath(), workingGzFile.toPath())
ConfigEncryptionTool tool = new ConfigEncryptionTool()
tool.isVerbose = true
tool.outputFlowXmlPath = workingGzFile.path.replaceAll("flow.xml.gz", "output.xml.gz")
String xmlContent = workingFile.text
logger.info("Read flow.xml: \n${xmlContent}")
// Act
tool.writeFlowXmlToFile(xmlContent)
// Assert
// Set the input path to what was just written and rely on the separately-tested load method to uncompress and read the contents
tool.flowXmlPath = tool.outputFlowXmlPath
assert tool.loadFlowXml() == xmlContent
}
@Test
void testShouldDetectActionFlags() {
// Arrange
@ -5159,6 +5269,17 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
}
}
@Test
void testFindFieldsInStream() {
def verifyTool = new ConfigEncryptionTool()
verifyTool.isVerbose = true
verifyTool.flowXmlPath = new File("src/test/resources/flow.xml.gz").path
InputStream updatedFlowXmlContent = verifyTool.loadFlowXml(verifyTool.flowXmlPath)
Set<String> fieldsFound = findFieldsInStream(updatedFlowXmlContent, WFXCTR)
logger.info("Found " + fieldsFound.size() + " fields in " + verifyTool.flowXmlPath + " that matched " + WFXCTR)
assert(fieldsFound.size() > 0)
}
static boolean compareXMLFragments(String expectedXML, String actualXML) {
Diff diffSimilar = DiffBuilder.compare(expectedXML).withTest(actualXML)
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName))
@ -5172,6 +5293,19 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
!diffSimilar.hasDifferences()
}
static Set<String> findFieldsInStream(InputStream fileInputStream, String pattern) {
Set<String> fieldsFound = new HashSet<String>()
Reader reader = new BufferedReader(new InputStreamReader(fileInputStream))
String line
while((line = reader.readLine()) != null) {
def matcher = line =~ pattern
if(matcher.find()) {
fieldsFound.add(matcher.getAt(0))
}
}
fieldsFound
}
// TODO: Test with 128/256-bit available
}

View File

@ -60,7 +60,7 @@
</property>
<property>
<name>Password</name>
<value>enc{2032416987A00D9FCD757528D7AE609D7E793CA5F956641DB53E14CDB9BFCD4037B73AC705CD3F5C1C1BDE18B8D7B281}</value>
<value>enc{7468697349734142616453616c742e2e79ad38319f8069990c6ac60cad75639f9d2ab7dff1a0b8b488af2fff659cf353}</value>
</property>
<property>
<name>raw-key-hex</name>
@ -112,7 +112,7 @@
</property>
<property>
<name>Password</name>
<value>enc{4B580C55B8355FE57A599B31B3B2ACA77429DBF6887C177417624026469E895F0C89FB0C9D9C64C5B2AD943035689C9C}</value>
<value>enc{7468697349734142616453616c742e2e63a2cd00e648c459e7e45223fefe8d38ab9ed3d71d94be57d5b0e4391980c858}</value>
</property>
<property>
<name>raw-key-hex</name>

View File

@ -60,7 +60,7 @@
</property>
<property>
<name>Password</name>
<value>enc{2032416987A00D9FCD757528D7AE609D7E793CA5F956641DB53E14CDB9BFCD4037B73AC705CD3F5C1C1BDE18B8D7B281}</value>
<value>enc{7468697349734142616453616c742e2e79ad38319f8069990c6ac60cad75639f9d2ab7dff1a0b8b488af2fff659cf353}</value>
</property>
<property>
<name>raw-key-hex</name>
@ -112,7 +112,7 @@
</property>
<property>
<name>Password</name>
<value>enc{4B580C55B8355FE57A599B31B3B2ACA77429DBF6887C177417624026469E895F0C89FB0C9D9C64C5B2AD943035689C9C}</value>
<value>enc{7468697349734142616453616c742e2e63a2cd00e648c459e7e45223fefe8d38ab9ed3d71d94be57d5b0e4391980c858}</value>
</property>
<property>
<name>raw-key-hex</name>