Repository: nifi
Updated Branches:
  refs/heads/master 3719a6268 -> 4e4aa54c6


NIFI-5116 Implemented logic to translate nifi.properties file to CLI properties 
format.
Added unit tests.

This closes #2660.

Signed-off-by: Bryan Bende <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/4e4aa54c
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/4e4aa54c
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/4e4aa54c

Branch: refs/heads/master
Commit: 4e4aa54c69ce991f7f7772cf95c666cd31610d23
Parents: 3719a62
Author: Andy LoPresto <[email protected]>
Authored: Wed Apr 25 18:27:05 2018 -0400
Committer: Bryan Bende <[email protected]>
Committed: Thu Apr 26 09:59:59 2018 -0400

----------------------------------------------------------------------
 .../nifi/properties/ConfigEncryptionTool.groovy | 117 ++++-
 .../properties/ConfigEncryptionToolTest.groovy  | 427 ++++++++++++++++++-
 2 files changed, 521 insertions(+), 23 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/4e4aa54c/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
index e8ac642..692669b 100644
--- 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
@@ -99,6 +99,7 @@ class ConfigEncryptionTool {
     private boolean handlingFlowXml = false
     private boolean ignorePropertiesFiles = false
     private boolean queryingCurrentHashParams = false
+    private boolean translatingCli = false
 
     private static final String HELP_ARG = "help"
     private static final String VERBOSE_ARG = "verbose"
@@ -124,6 +125,7 @@ class ConfigEncryptionTool {
     private static final String NEW_FLOW_ALGORITHM_ARG = "newFlowAlgorithm"
     private static final String NEW_FLOW_PROVIDER_ARG = "newFlowProvider"
     private static final String CURRENT_HASH_PARAMS_ARG = "currentHashParams"
+    private static final String TRANSLATE_CLI_ARG = "translateCli"
 
     // Static holder to avoid re-generating the options object multiple times 
in an invocation
     private static Options staticOptions
@@ -198,7 +200,17 @@ class ConfigEncryptionTool {
 
     private static final String DEFAULT_PROVIDER = 
BouncyCastleProvider.PROVIDER_NAME
     private static final String DEFAULT_FLOW_ALGORITHM = 
"PBEWITHMD5AND256BITAES-CBC-OPENSSL"
-    static private final int AMBARI_COMPATIBLE_SCRYPT_HASH_LENGTH = 256
+    private static final int AMBARI_COMPATIBLE_SCRYPT_HASH_LENGTH = 256
+
+    private static final Map<String, String> PROPERTY_KEY_MAP = [
+            "nifi.security.keystore": "keystore",
+            "nifi.security.keystoreType": "keystoreType",
+            "nifi.security.keystorePasswd": "keystorePasswd",
+            "nifi.security.keyPasswd": "keyPasswd",
+            "nifi.security.truststore": "truststore",
+            "nifi.security.truststoreType": "truststoreType",
+            "nifi.security.truststorePasswd": "truststorePasswd",
+    ]
 
     private static String buildHeader(String description = 
DEFAULT_DESCRIPTION) {
         "${SEP}${description}${SEP * 2}"
@@ -247,6 +259,7 @@ class ConfigEncryptionTool {
         
options.addOption(Option.builder("A").longOpt(NEW_FLOW_ALGORITHM_ARG).hasArg(true).argName("algorithm").desc("The
 algorithm to use to encrypt the sensitive processor properties in 
flow.xml.gz").build())
         
options.addOption(Option.builder("P").longOpt(NEW_FLOW_PROVIDER_ARG).hasArg(true).argName("algorithm").desc("The
 security provider to use to encrypt the sensitive processor properties in 
flow.xml.gz").build())
         
options.addOption(Option.builder().longOpt(CURRENT_HASH_PARAMS_ARG).hasArg(false).desc("Returns
 the current salt and cost params used to store the hashed 
key/password").build())
+        
options.addOption(Option.builder("c").longOpt(TRANSLATE_CLI_ARG).hasArg(false).desc("Translates
 the nifi.properties file to a format suitable for the NiFi CLI tool").build())
         options
     }
 
@@ -302,8 +315,44 @@ class ConfigEncryptionTool {
                 }
             }
 
+            // If this flag is present, ensure no other options are present 
and then fail/return
+            if (commandLine.hasOption(TRANSLATE_CLI_ARG)) {
+                translatingCli = true
+                if (commandLineHasActionFlags(commandLine, [TRANSLATE_CLI_ARG, 
BOOTSTRAP_CONF_ARG, NIFI_PROPERTIES_ARG])) {
+                    printUsageAndThrow("When '-c'/'--${TRANSLATE_CLI_ARG}' is 
specified, only '-h', '-v', and '-n'/'-b' with the relevant files are allowed", 
ExitCode.INVALID_ARGS)
+                }
+            }
+
             bootstrapConfPath = commandLine.getOptionValue(BOOTSTRAP_CONF_ARG)
 
+            // This needs to occur even if the nifi.properties won't be 
encrypted
+            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
+                boolean ignoreFlagPresent = 
commandLine.hasOption(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG)
+                if (isVerbose && !ignoreFlagPresent) {
+                    logger.info("Handling encryption of nifi.properties")
+                }
+                niFiPropertiesPath = 
commandLine.getOptionValue(NIFI_PROPERTIES_ARG)
+                outputNiFiPropertiesPath = 
commandLine.getOptionValue(OUTPUT_NIFI_PROPERTIES_ARG, niFiPropertiesPath)
+                handlingNiFiProperties = !ignoreFlagPresent
+
+                if (niFiPropertiesPath == outputNiFiPropertiesPath) {
+                    // TODO: Add confirmation pause and provide -y flag to 
offer no-interaction mode?
+                    logger.warn("The source nifi.properties and destination 
nifi.properties are identical [${outputNiFiPropertiesPath}] so the original 
will be overwritten")
+                }
+            }
+
+            // If translating nifi.properties to CLI format, none of the 
remaining parsing is necessary
+            if (translatingCli) {
+
+                // If the nifi.properties isn't present, throw an exception
+                // If the nifi.properties is encrypted and the bootstrap.conf 
isn't present, we will throw an error later when the encryption is detected
+                if (!niFiPropertiesPath) {
+                    printUsageAndThrow("When '-c'/'--translateCli' is 
specified, '-n'/'--niFiProperties' is required (and '-b'/'--bootstrapConf' is 
required if the properties are encrypted)", ExitCode.INVALID_ARGS)
+                }
+
+                return commandLine
+            }
+
             // If this flag is provided, the nifi.properties is necessary to 
read/write the flow encryption key, but the encryption process will not 
actually be applied to nifi.properties / login-identity-providers.xml
             if (commandLine.hasOption(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG)) {
                 handlingNiFiProperties = false
@@ -339,22 +388,6 @@ class ConfigEncryptionTool {
                 }
             }
 
-            // This needs to occur even if the nifi.properties won't be 
encrypted
-            if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) {
-                boolean ignoreFlagPresent = 
commandLine.hasOption(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG)
-                if (isVerbose && !ignoreFlagPresent) {
-                    logger.info("Handling encryption of nifi.properties")
-                }
-                niFiPropertiesPath = 
commandLine.getOptionValue(NIFI_PROPERTIES_ARG)
-                outputNiFiPropertiesPath = 
commandLine.getOptionValue(OUTPUT_NIFI_PROPERTIES_ARG, niFiPropertiesPath)
-                handlingNiFiProperties = !ignoreFlagPresent
-
-                if (niFiPropertiesPath == outputNiFiPropertiesPath) {
-                    // TODO: Add confirmation pause and provide -y flag to 
offer no-interaction mode?
-                    logger.warn("The source nifi.properties and destination 
nifi.properties are identical [${outputNiFiPropertiesPath}] so the original 
will be overwritten")
-                }
-            }
-
             if (commandLine.hasOption(FLOW_XML_ARG)) {
                 if (isVerbose) {
                     logger.info("Handling encryption of flow.xml.gz")
@@ -479,6 +512,13 @@ class ConfigEncryptionTool {
         return commandLine
     }
 
+    /**
+     * Returns true if the {@code commandLine} object has flags other than the 
{@code help} or {@code verbose} flags or any of the acceptable args provided in 
an optional parameter. This is used to detect incompatible arguments for 
specific modes.
+     *
+     * @param commandLine the commandLine object
+     * @param acceptableOptionStrings an optional list of acceptable options 
that can be present without returning true
+     * @return true if incompatible flags are present
+     */
     boolean commandLineHasActionFlags(CommandLine commandLine, List<String> 
acceptableOptionStrings = []) {
         // Resolve the list of Option objects corresponding to "help" and 
"verbose"
         final List<Option> ALWAYS_ACCEPTABLE_OPTIONS = 
resolveOptions([HELP_ARG, VERBOSE_ARG])
@@ -1650,6 +1690,26 @@ class ConfigEncryptionTool {
                     System.exit(ExitCode.SUCCESS.ordinal())
                 }
 
+                // Handle the translate CLI case
+                if (tool.translatingCli) {
+                    if (tool.bootstrapConfPath) {
+                        // Check to see if bootstrap.conf has a master key
+                        tool.keyHex = 
NiFiPropertiesLoader.extractKeyFromBootstrapFile(tool.bootstrapConfPath)
+                    }
+
+                    if (!tool.keyHex) {
+                        logger.info("No master key detected in 
${tool.bootstrapConfPath} -- if ${tool.niFiPropertiesPath} is encrypted, the 
translation will fail")
+                    }
+
+                    // Load the existing properties (decrypting if necessary)
+                    tool.niFiProperties = tool.loadNiFiProperties(tool.keyHex)
+
+                    String cliOutput = tool.translateNiFiPropertiesToCLI()
+
+                    System.out.println(cliOutput)
+                    System.exit(ExitCode.SUCCESS.ordinal())
+                }
+
                 boolean existingNiFiPropertiesAreEncrypted = 
tool.niFiPropertiesAreEncrypted()
                 if (!tool.ignorePropertiesFiles || (tool.handlingFlowXml && 
existingNiFiPropertiesAreEncrypted)) {
                     // If we are handling the flow.xml.gz and nifi.properties 
is already encrypted, try getting the key from bootstrap.conf rather than the 
console
@@ -1820,4 +1880,27 @@ class ConfigEncryptionTool {
 
         System.exit(ExitCode.SUCCESS.ordinal())
     }
+
+    String translateNiFiPropertiesToCLI() {
+        // Assemble the baseUrl
+        String baseUrl = determineBaseUrl(niFiProperties)
+
+        // Copy the relevant properties to a Map using the "CLI" keys
+        List<String> cliOutput = ["baseUrl=${baseUrl}"]
+        PROPERTY_KEY_MAP.each { String nfpKey, String cliKey ->
+            cliOutput << "${cliKey}=${niFiProperties.getProperty(nfpKey)}"
+        }
+
+        cliOutput << "proxiedEntity="
+
+        cliOutput.join("\n")
+    }
+
+    static String determineBaseUrl(NiFiProperties niFiProperties) {
+        String protocol = niFiProperties.isHTTPSConfigured() ? "https" : "http"
+        String host = niFiProperties.isHTTPSConfigured() ? 
niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_HOST) : 
niFiProperties.getProperty(NiFiProperties.WEB_HTTP_HOST)
+        String port = niFiProperties.getConfiguredHttpOrHttpsPort()
+
+        "${protocol}://${host}:${port}"
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/4e4aa54c/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
index 12e88e6..77f5c8c 100644
--- 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
@@ -2221,7 +2221,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
     /**
      * Ideally all of the combination tests would be a single test with 
iterative argument lists, but due to the System.exit(), it can only be captured 
once per test.
      */
-    @Ignore  // TODO re-enable once this is passing on all platforms
+    @Ignore
+    // TODO re-enable once this is passing on all platforms
     @Test
     void testShouldMigrateFromHashedPasswordToPassword() {
         // Arrange
@@ -2236,7 +2237,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         // Assertions in common method above
     }
 
-    @Ignore  // TODO re-enable once this is passing on all platforms
+    @Ignore
+    // TODO re-enable once this is passing on all platforms
     @Test
     void testShouldMigrateFromHashedPasswordToKey() {
         // Arrange
@@ -2251,7 +2253,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         // Assertions in common method above
     }
 
-    @Ignore  // TODO re-enable once this is passing on all platforms
+    @Ignore
+    // TODO re-enable once this is passing on all platforms
     @Test
     void testShouldMigrateFromHashedKeyToPassword() {
         // Arrange
@@ -2266,7 +2269,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         // Assertions in common method above
     }
 
-    @Ignore  // TODO re-enable once this is passing on all platforms
+    @Ignore
+    // TODO re-enable once this is passing on all platforms
     @Test
     void testShouldMigrateFromHashedKeyToKey() {
         // Arrange
@@ -2281,7 +2285,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         // Assertions in common method above
     }
 
-    @Ignore  // TODO re-enable once this is passing on all platforms
+    @Ignore
+    // TODO re-enable once this is passing on all platforms
     @Test
     void testShouldFailToMigrateFromIncorrectHashedPasswordToPassword() {
         // Arrange
@@ -5205,7 +5210,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         }
     }
 
-    @Ignore  // TODO re-enable once this is passing on all platforms
+    @Ignore
+    // TODO re-enable once this is passing on all platforms
     @Test
     void testShouldReturnCurrentHashParams() {
         // Arrange
@@ -5349,6 +5355,415 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         }
     }
 
+    @Test
+    void testShouldTranslateCliWithPlaintextInput() {
+        // Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        final Map<String, String> EXPECTED_CLI_OUTPUT = [
+                "baseUrl"         : "https://nifi.nifi.apache.org:8443";,
+                "keystore"        : "/path/to/keystore.jks",
+                "keystoreType"    : "JKS",
+                "keystorePasswd"  : "thisIsABadKeystorePassword",
+                "keyPasswd"       : "thisIsABadKeyPassword",
+                "truststore"      : "",
+                "truststoreType"  : "",
+                "truststorePasswd": "",
+                "proxiedEntity"   : "",
+        ]
+
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, 
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, 
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, 
PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, 
PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE])
+
+        File emptyKeyFile = new 
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(emptyKeyFile.toPath(), bootstrapFile.toPath())
+        final List<String> originalBootstrapLines = bootstrapFile.readLines()
+        String originalKeyLine = originalBootstrapLines.find {
+            it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+        }
+        logger.info("Original key line from bootstrap.conf: 
${originalKeyLine}")
+        assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX
+
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+
+        NiFiProperties inputProperties = new 
NiFiPropertiesLoader().load(inputPropertiesFile)
+        logger.info("Loaded ${inputProperties.size()} properties from input 
file")
+        ProtectedNiFiProperties protectedInputProperties = new 
ProtectedNiFiProperties(inputProperties)
+        def originalSensitiveValues = 
protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key 
-> [(key): protectedInputProperties.getProperty(key)] }
+        logger.info("Original sensitive values: ${originalSensitiveValues}")
+
+        String[] args = ["-n", inputPropertiesFile.path, "-b", 
bootstrapFile.path, "-c"]
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            void checkAssertion() {
+                final String standardOutput = systemOutRule.getLog()
+                List<String> lines = standardOutput.split("\n")
+
+                // The SystemRule log also includes STDERR, so truncate after 
9 lines
+                def stdoutLines = lines[0..<EXPECTED_CLI_OUTPUT.size()]
+                logger.info("STDOUT:\n\t${stdoutLines.join("\n\t")}")
+
+                // Split the output into lines and create a map of the keys 
and values
+                def parsedCli = stdoutLines.collectEntries { String line ->
+                    def components = line.split("=", 2)
+                    components.size() > 1 ? [(components[0]): components[1]] : 
[(components[0]): ""]
+                }
+
+                assert parsedCli.size() == EXPECTED_CLI_OUTPUT.size()
+                assert EXPECTED_CLI_OUTPUT.every { String k, String v -> 
parsedCli.get(k) == v }
+
+                // Clean up
+                bootstrapFile.deleteOnExit()
+                tmpDir.deleteOnExit()
+            }
+        })
+
+        // Act
+        ConfigEncryptionTool.main(args)
+        logger.info("Invoked #main with ${args.join(" ")}")
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testShouldTranslateCliWithPlaintextInputWithoutBootstrapConf() {
+        // Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        final Map<String, String> EXPECTED_CLI_OUTPUT = [
+                "baseUrl"         : "https://nifi.nifi.apache.org:8443";,
+                "keystore"        : "/path/to/keystore.jks",
+                "keystoreType"    : "JKS",
+                "keystorePasswd"  : "thisIsABadKeystorePassword",
+                "keyPasswd"       : "thisIsABadKeyPassword",
+                "truststore"      : "",
+                "truststoreType"  : "",
+                "truststorePasswd": "",
+                "proxiedEntity"   : "",
+        ]
+
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, 
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, 
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, 
PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, 
PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE])
+
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+
+        NiFiProperties inputProperties = new 
NiFiPropertiesLoader().load(inputPropertiesFile)
+        logger.info("Loaded ${inputProperties.size()} properties from input 
file")
+        ProtectedNiFiProperties protectedInputProperties = new 
ProtectedNiFiProperties(inputProperties)
+        def originalSensitiveValues = 
protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key 
-> [(key): protectedInputProperties.getProperty(key)] }
+        logger.info("Original sensitive values: ${originalSensitiveValues}")
+
+        String[] args = ["-n", inputPropertiesFile.path, "-c"]
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            void checkAssertion() {
+                final String standardOutput = systemOutRule.getLog()
+                List<String> lines = standardOutput.split("\n")
+
+                // The SystemRule log also includes STDERR, so truncate after 
9 lines
+                def stdoutLines = lines[0..<EXPECTED_CLI_OUTPUT.size()]
+                logger.info("STDOUT:\n\t${stdoutLines.join("\n\t")}")
+
+                // Split the output into lines and create a map of the keys 
and values
+                def parsedCli = stdoutLines.collectEntries { String line ->
+                    def components = line.split("=", 2)
+                    components.size() > 1 ? [(components[0]): components[1]] : 
[(components[0]): ""]
+                }
+
+                assert parsedCli.size() == EXPECTED_CLI_OUTPUT.size()
+                assert EXPECTED_CLI_OUTPUT.every { String k, String v -> 
parsedCli.get(k) == v }
+
+                // Clean up
+                bootstrapFile.deleteOnExit()
+                tmpDir.deleteOnExit()
+            }
+        })
+
+        // Act
+        ConfigEncryptionTool.main(args)
+        logger.info("Invoked #main with ${args.join(" ")}")
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testShouldTranslateCliWithEncryptedInput() {
+        // Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        final Map<String, String> EXPECTED_CLI_OUTPUT = [
+                "baseUrl"         : "https://nifi.nifi.apache.org:8443";,
+                "keystore"        : "/path/to/keystore.jks",
+                "keystoreType"    : "JKS",
+                "keystorePasswd"  : "thisIsABadKeystorePassword",
+                "keyPasswd"       : "thisIsABadKeyPassword",
+                "truststore"      : "",
+                "truststoreType"  : "",
+                "truststorePasswd": "",
+                "proxiedEntity"   : "",
+        ]
+
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, 
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, 
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, 
PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, 
PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE])
+
+        File masterKeyFile = new 
File("src/test/resources/bootstrap_with_master_key.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(masterKeyFile.toPath(), bootstrapFile.toPath())
+
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_protected_aes.properties")
+
+        NiFiProperties inputProperties = 
NiFiPropertiesLoader.withKey(KEY_HEX).load(inputPropertiesFile)
+        logger.info("Loaded ${inputProperties.size()} properties from input 
file")
+        ProtectedNiFiProperties protectedInputProperties = new 
ProtectedNiFiProperties(inputProperties)
+        def originalSensitiveValues = 
protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key 
-> [(key): protectedInputProperties.getProperty(key)] }
+        logger.info("Original sensitive values: ${originalSensitiveValues}")
+
+        String[] args = ["-n", inputPropertiesFile.path, "-b", 
bootstrapFile.path, "-c"]
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            void checkAssertion() {
+                final String standardOutput = systemOutRule.getLog()
+                List<String> lines = standardOutput.split("\n")
+
+                // The SystemRule log also includes STDERR, so truncate after 
9 lines
+                def stdoutLines = lines[0..<EXPECTED_CLI_OUTPUT.size()]
+                logger.info("STDOUT:\n\t${stdoutLines.join("\n\t")}")
+
+                // Split the output into lines and create a map of the keys 
and values
+                def parsedCli = stdoutLines.collectEntries { String line ->
+                    def components = line.split("=", 2)
+                    components.size() > 1 ? [(components[0]): components[1]] : 
[(components[0]): ""]
+                }
+
+                assert parsedCli.size() == EXPECTED_CLI_OUTPUT.size()
+                assert EXPECTED_CLI_OUTPUT.every { String k, String v -> 
parsedCli.get(k) == v }
+
+                // Clean up
+                bootstrapFile.deleteOnExit()
+                tmpDir.deleteOnExit()
+            }
+        })
+
+        // Act
+        ConfigEncryptionTool.main(args)
+        logger.info("Invoked #main with ${args.join(" ")}")
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testTranslateCliWithEncryptedInputShouldNotIntersperseVerboseOutput() 
{
+        // Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        final Map<String, String> EXPECTED_CLI_OUTPUT = [
+                "baseUrl"         : "https://nifi.nifi.apache.org:8443";,
+                "keystore"        : "/path/to/keystore.jks",
+                "keystoreType"    : "JKS",
+                "keystorePasswd"  : "thisIsABadKeystorePassword",
+                "keyPasswd"       : "thisIsABadKeyPassword",
+                "truststore"      : "",
+                "truststoreType"  : "",
+                "truststorePasswd": "",
+                "proxiedEntity"   : "",
+        ]
+
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, 
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE, 
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, 
PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, 
PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE])
+
+        File masterKeyFile = new 
File("src/test/resources/bootstrap_with_master_key.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(masterKeyFile.toPath(), bootstrapFile.toPath())
+
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_protected_aes.properties")
+
+        NiFiProperties inputProperties = 
NiFiPropertiesLoader.withKey(KEY_HEX).load(inputPropertiesFile)
+        logger.info("Loaded ${inputProperties.size()} properties from input 
file")
+        ProtectedNiFiProperties protectedInputProperties = new 
ProtectedNiFiProperties(inputProperties)
+        def originalSensitiveValues = 
protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key 
-> [(key): protectedInputProperties.getProperty(key)] }
+        logger.info("Original sensitive values: ${originalSensitiveValues}")
+
+        String[] args = ["-n", inputPropertiesFile.path, "-b", 
bootstrapFile.path, "-c", "-v"]
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            void checkAssertion() {
+                final String standardOutput = systemOutRule.getLog()
+                List<String> lines = standardOutput.split("\n")
+
+                // The SystemRule log also includes STDERR, so truncate after 
9 lines
+                def stdoutLines = lines[0..<EXPECTED_CLI_OUTPUT.size()]
+                logger.info("STDOUT:\n\t${stdoutLines.join("\n\t")}")
+
+                // Split the output into lines and create a map of the keys 
and values
+                def parsedCli = stdoutLines.collectEntries { String line ->
+                    def components = line.split("=", 2)
+                    components.size() > 1 ? [(components[0]): components[1]] : 
[(components[0]): ""]
+                }
+
+                assert parsedCli.size() == EXPECTED_CLI_OUTPUT.size()
+                assert EXPECTED_CLI_OUTPUT.every { String k, String v -> 
parsedCli.get(k) == v }
+
+                // Clean up
+                bootstrapFile.deleteOnExit()
+                tmpDir.deleteOnExit()
+            }
+        })
+
+        // Act
+        ConfigEncryptionTool.main(args)
+        logger.info("Invoked #main with ${args.join(" ")}")
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testShouldTranslateCli() {
+        // Arrange
+        final Map<String, String> EXPECTED_CLI_OUTPUT = [
+                "baseUrl"         : "https://nifi.nifi.apache.org:8443";,
+                "keystore"        : "/path/to/keystore.jks",
+                "keystoreType"    : "JKS",
+                "keystorePasswd"  : "thisIsABadKeystorePassword",
+                "keyPasswd"       : "thisIsABadKeyPassword",
+                "truststore"      : "",
+                "truststoreType"  : "",
+                "truststorePasswd": "",
+                "proxiedEntity"   : "",
+        ]
+
+        String originalNiFiPropertiesPath = 
"src/test/resources/nifi_with_sensitive_properties_unprotected.properties"
+
+        NiFiProperties plainProperties = 
NiFiPropertiesLoader.withKey(KEY_HEX).load(originalNiFiPropertiesPath)
+        logger.info("Loaded NiFiProperties from ${originalNiFiPropertiesPath}")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.translatingCli = true
+        tool.niFiProperties = plainProperties
+
+        // Act
+        String cliOutput = tool.translateNiFiPropertiesToCLI()
+        logger.info("Translated to CLI format: \n${cliOutput}")
+
+        // Assert
+        def parsedCli = cliOutput.split("\n").collectEntries { String line ->
+            def components = line.split("=", 2)
+            [(components[0]): components[1]]
+        }
+
+        assert parsedCli.size() == EXPECTED_CLI_OUTPUT.size()
+        assert EXPECTED_CLI_OUTPUT.every { String k, String v -> 
parsedCli.get(k) == v }
+    }
+
+    @Test
+    void testShouldFailOnCliTranslateIfConflictingFlagsPresent() {
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        def validOpts = [
+                "-n nifi.properties",
+                "--niFiProperties nifi.properties",
+                "--verbose -n nifi.properties -b bootstrap.conf",
+        ]
+
+        // These values won't cause an error in #commandLineHasActionFlags() 
but will throw an error later in #parse()
+        // Don't test with -h/--help because it will cause a System.exit()
+        def incompleteOpts = [
+                "",
+                "-v",
+                "--verbose",
+//                "-h",
+//                "--help",
+                "-b bootstrap.conf",
+                "--bootstrapConf bootstrap.conf",
+        ]
+
+        def invalidOpts = [
+                "--migrate",
+                "-o output",
+                "-x \$s0\$"
+        ]
+
+        // Act
+        validOpts.each { String valid ->
+            tool = new ConfigEncryptionTool()
+            def args = (valid + " -c").split(" ")
+            logger.info("Testing with ${args}")
+            tool.parse(args as String[])
+        }
+
+        incompleteOpts.each { String incomplete ->
+            tool = new ConfigEncryptionTool()
+            def args = (incomplete + " -c").split(" ")
+            logger.info("Testing with ${args}")
+            def msg = shouldFail(CommandLineParseException) {
+                tool.parse(args as String[])
+            }
+
+            // Assert
+            assert msg == "When '-c'/'--translateCli' is specified, 
'-n'/'--niFiProperties' is required (and '-b'/'--bootstrapConf' is required if 
the properties are encrypted)"
+            assert systemOutRule.getLog().contains("usage: 
org.apache.nifi.properties.ConfigEncryptionTool [")
+        }
+
+        invalidOpts.each { String invalid ->
+            tool = new ConfigEncryptionTool()
+            def args = (invalid + " -c").split(" ")
+            logger.info("Testing with ${args}")
+            def msg = shouldFail(CommandLineParseException) {
+                tool.parse(args as String[])
+            }
+
+            // Assert
+            assert msg == "When '-c'/'--translateCli' is specified, only '-h', 
'-v', and '-n'/'-b' with the relevant files are allowed"
+            assert systemOutRule.getLog().contains("usage: 
org.apache.nifi.properties.ConfigEncryptionTool [")
+        }
+    }
+
+    @Test
+    void testTranslateCliShouldFailIfMissingNecessaryFlags() {
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Bootstrap alone is insufficient; nifi.properties alone is ok if it 
is in plaintext
+        def invalidOpts = [
+                "-b bootstrap.conf",
+        ]
+
+        // Act
+        invalidOpts.each { String invalid ->
+            def args = (invalid + " -c").split(" ")
+            logger.info("Testing with ${args}")
+            def msg = shouldFail(CommandLineParseException) {
+                tool.parse(args as String[])
+            }
+
+            // Assert
+            assert msg == "When '-c'/'--translateCli' is specified, 
'-n'/'--niFiProperties' is required (and '-b'/'--bootstrapConf' is required if 
the properties are encrypted)"
+            assert systemOutRule.getLog().contains("usage: 
org.apache.nifi.properties.ConfigEncryptionTool [")
+        }
+    }
+
 // TODO: Test with 128/256-bit available
 }
 

Reply via email to