NIFI-4573 This closes #3460. Refactored logic handling flow XML encryption migration.

Added unit tests.

Signed-off-by: Joe Witt <joewitt@apache.org>
This commit is contained in:
Andy LoPresto 2019-05-02 18:53:03 -07:00 committed by Joe Witt
parent c0de26b8d6
commit c2746fac5f
No known key found for this signature in database
GPG Key ID: 9093BF854F811A1A
2 changed files with 158 additions and 50 deletions

View File

@ -39,6 +39,7 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.xml.sax.SAXException
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
@ -1536,58 +1537,12 @@ class ConfigEncryptionTool {
try {
tool.flowXml = tool.loadFlowXml()
} catch (Exception e) {
if (tool.isVerbose) {
logger.error("Encountered an error: ", e)
}
tool.printUsageAndThrow("Cannot load flow.xml.gz", ExitCode.ERROR_READING_NIFI_PROPERTIES)
}
// If the flow password was not set in nifi.properties, use the hard-coded default
String existingFlowPassword = tool.getExistingFlowPassword()
// If the new password was not provided in the arguments, read from the console. If that is empty, use the same value (essentially a copy no-op)
String newFlowPassword = tool.flowPropertiesPassword ?: tool.getFlowPassword()
if (!newFlowPassword) {
newFlowPassword = existingFlowPassword
}
// Get the algorithms and providers
NiFiProperties nfp = tool.niFiProperties
String existingAlgorithm = nfp?.getProperty(NiFiProperties.SENSITIVE_PROPS_ALGORITHM) ?: DEFAULT_FLOW_ALGORITHM
String existingProvider = nfp?.getProperty(NiFiProperties.SENSITIVE_PROPS_PROVIDER) ?: DEFAULT_PROVIDER
String newAlgorithm = tool.newFlowAlgorithm ?: existingAlgorithm
String newProvider = tool.newFlowProvider ?: existingProvider
try {
tool.flowXml = tool.migrateFlowXmlContent(tool.flowXml, existingFlowPassword, newFlowPassword, existingAlgorithm, existingProvider, newAlgorithm, newProvider)
} catch (Exception e) {
if (tool.isVerbose) {
logger.error("Encountered an error", e, "Check your password?")
}
tool.printUsageAndThrow("Encountered an error migrating flow content", ExitCode.ERROR_MIGRATING_FLOW)
}
// If the new key is the hard-coded internal value, don't persist it to nifi.properties
if (newFlowPassword != DEFAULT_NIFI_SENSITIVE_PROPS_KEY && newFlowPassword != existingFlowPassword) {
// Update the NiFiProperties object with the new flow password before it gets encrypted (wasteful, but NiFiProperties instances are immutable)
Properties rawProperties = new Properties()
nfp.getPropertyKeys().each { String k ->
rawProperties.put(k, nfp.getProperty(k))
}
// If the tool is not going to encrypt NiFiProperties and the existing file is already encrypted, encrypt and update the new sensitive props key
if (!tool.handlingNiFiProperties && existingNiFiPropertiesAreEncrypted) {
AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(tool.keyHex)
String encryptedSPK = spp.protect(newFlowPassword)
rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, encryptedSPK)
// Manually update the protection scheme or it will be lost
rawProperties.put(ProtectedNiFiProperties.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY), spp.getIdentifierKey())
if (tool.isVerbose) {
logger.info("Tool is not configured to encrypt nifi.properties, but the existing nifi.properties is encrypted and flow.xml.gz was migrated, so manually persisting the new encrypted value to nifi.properties")
}
} else {
rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, newFlowPassword)
}
tool.niFiProperties = new StandardNiFiProperties(rawProperties)
}
tool.handleFlowXml(existingNiFiPropertiesAreEncrypted)
}
if (tool.handlingNiFiProperties) {
@ -1637,6 +1592,62 @@ class ConfigEncryptionTool {
System.exit(ExitCode.SUCCESS.ordinal())
}
void handleFlowXml(boolean existingNiFiPropertiesAreEncrypted = false) {
// If the flow password was not set in nifi.properties, use the hard-coded default
String existingFlowPassword = getExistingFlowPassword()
// If the new password was not provided in the arguments, read from the console. If that is empty, use the same value (essentially a copy no-op)
String newFlowPassword = flowPropertiesPassword ?: getFlowPassword()
if (!newFlowPassword) {
newFlowPassword = existingFlowPassword
}
// Get the algorithms and providers
NiFiProperties nfp = niFiProperties
String existingAlgorithm = nfp?.getProperty(NiFiProperties.SENSITIVE_PROPS_ALGORITHM) ?: DEFAULT_FLOW_ALGORITHM
String existingProvider = nfp?.getProperty(NiFiProperties.SENSITIVE_PROPS_PROVIDER) ?: DEFAULT_PROVIDER
String newAlgorithm = newFlowAlgorithm ?: existingAlgorithm
String newProvider = newFlowProvider ?: existingProvider
try {
flowXml = migrateFlowXmlContent(flowXml, existingFlowPassword, newFlowPassword, existingAlgorithm, existingProvider, newAlgorithm, newProvider)
} catch (Exception e) {
logger.error("Encountered an error: ${e.getLocalizedMessage()}")
if (e instanceof BadPaddingException) {
logger.error("This error is likely caused by providing the wrong existing flow password. Check that the existing flow password [-p] is the one used to encrypt the provided flow.xml.gz file")
}
if (isVerbose) {
logger.error("Exception: ", e)
}
printUsageAndThrow("Encountered an error migrating flow content", ExitCode.ERROR_MIGRATING_FLOW)
}
// If the new key is the hard-coded internal value, don't persist it to nifi.properties
if (newFlowPassword != DEFAULT_NIFI_SENSITIVE_PROPS_KEY && newFlowPassword != existingFlowPassword) {
// Update the NiFiProperties object with the new flow password before it gets encrypted (wasteful, but NiFiProperties instances are immutable)
Properties rawProperties = new Properties()
nfp.getPropertyKeys().each { String k ->
rawProperties.put(k, nfp.getProperty(k))
}
// If the tool is not going to encrypt NiFiProperties and the existing file is already encrypted, encrypt and update the new sensitive props key
if (!handlingNiFiProperties && existingNiFiPropertiesAreEncrypted) {
AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(keyHex)
String encryptedSPK = spp.protect(newFlowPassword)
rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, encryptedSPK)
// Manually update the protection scheme or it will be lost
rawProperties.put(ProtectedNiFiProperties.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY), spp.getIdentifierKey())
if (isVerbose) {
logger.info("Tool is not configured to encrypt nifi.properties, but the existing nifi.properties is encrypted and flow.xml.gz was migrated, so manually persisting the new encrypted value to nifi.properties")
}
} else {
rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, newFlowPassword)
}
niFiProperties = new StandardNiFiProperties(rawProperties)
}
}
String translateNiFiPropertiesToCLI() {
// Assemble the baseUrl
String baseUrl = determineBaseUrl(niFiProperties)

View File

@ -38,6 +38,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.contrib.java.lang.system.Assertion
import org.junit.contrib.java.lang.system.ExpectedSystemExit
import org.junit.contrib.java.lang.system.SystemErrRule
import org.junit.contrib.java.lang.system.SystemOutRule
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@ -48,6 +49,7 @@ import org.xmlunit.diff.DefaultNodeMatcher
import org.xmlunit.diff.Diff
import org.xmlunit.diff.ElementSelectors
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
@ -68,6 +70,9 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
@Rule
public final SystemOutRule systemOutRule = new SystemOutRule().enableLog()
@Rule
public final SystemErrRule systemErrRule = new SystemErrRule().enableLog()
private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
private static final String KEY_HEX_256 = KEY_HEX_128 * 2
public static final String KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? KEY_HEX_256 : KEY_HEX_128
@ -4527,6 +4532,98 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
}
}
/**
* This test is tightly scoped to the migration of the flow XML content to ensure the expected exception type is thrown.
*/
@Test
void testMigrateFlowXmlContentWithIncorrectExistingPasswordShouldFailWithBadPaddingException() {
// Arrange
String flowXmlPath = "src/test/resources/flow.xml"
File flowXmlFile = new File(flowXmlPath)
File tmpDir = setupTmpDir()
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
String xmlContent = workingFile.text
// Act
def message = shouldFail(BadPaddingException) {
String migratedXmlContent = tool.migrateFlowXmlContent(xmlContent, wrongExistingFlowPassword, newFlowPassword)
logger.info("Migrated flow.xml: \n${migratedXmlContent}")
}
logger.expected(message)
// Assert
assert message =~ "pad block corrupted"
}
/**
* This test is scoped to the higher-level method to ensure that if a bad padding exception is thrown, the right errors are displayed.
*/
@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()
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.niFiProperties = nifiProperties
tool.flowPropertiesPassword = newFlowPassword
tool.handlingNiFiProperties = false
// Act
def message = shouldFail(Exception) {
tool.handleFlowXml()
logger.info("Migrated flow.xml: \n${tool.flowXml}")
}
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"
}
private static StandardNiFiProperties wrapNFP(Map<String, String> map) {
new StandardNiFiProperties(
new Properties(map))
}
@Test
void testShouldLoadFlowXmlContent() {
// Arrange