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 }
