[ https://issues.apache.org/jira/browse/NIFI-4701?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16306969#comment-16306969 ]
ASF GitHub Bot commented on NIFI-4701: -------------------------------------- Github user alopresto commented on a diff in the pull request: https://github.com/apache/nifi/pull/2350#discussion_r159130193 --- Diff: nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy --- @@ -2506,92 +2604,789 @@ class ConfigEncryptionToolTest extends GroovyTestCase { } @Test - void testShouldPerformFullOperationForNiFiPropertiesAndLoginIdentityProviders() { + void testShouldDecryptAuthorizers() { // Arrange - exit.expectSystemExitWithStatus(0) + String authorizersPath = "src/test/resources/authorizers-populated-encrypted.xml" + File authorizersFile = new File(authorizersPath) - File tmpDir = setupTmpDir() + setupTmpDir() - File emptyKeyFile = new File("src/test/resources/bootstrap_with_empty_master_key.conf") - File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf") - bootstrapFile.delete() + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() - 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 + // Sanity check for decryption + String cipherText = "q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA" + String EXPECTED_PASSWORD = "thisIsABadPassword" + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX_128) + assert spp.unprotect(cipherText) == EXPECTED_PASSWORD - final String EXPECTED_KEY_LINE = ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX + tool.keyHex = KEY_HEX_128 - // Set up the NFP file - File inputPropertiesFile = new File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties") - File outputPropertiesFile = new File("target/tmp/tmp_nifi.properties") - outputPropertiesFile.delete() + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") - 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}") + // Act + def decryptedLines = tool.decryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") - // Set up the LIP file - File inputLIPFile = new File("src/test/resources/login-identity-providers-populated.xml") - File outputLIPFile = new File("target/tmp/tmp-lip.xml") - outputLIPFile.delete() + // Assert + def passwordLines = decryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + assert passwordLines.every { it =~ ">thisIsABadPassword<" } + // Some lines were not encrypted originally so the encryption attribute would not have been updated + assert passwordLines.any { it =~ "encryption=\"none\"" } + } - String originalXmlContent = inputLIPFile.text - logger.info("Original XML content: ${originalXmlContent}") + @Test + void testShouldDecryptAuthorizersWithMultilineElements() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-populated-encrypted-multiline.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def decryptedLines = tool.decryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") + + // Assert + def passwordLines = decryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + assert passwordLines.every { it =~ ">thisIsABadPassword<" } + // Some lines were not encrypted originally so the encryption attribute would not have been updated + assert passwordLines.any { it =~ "encryption=\"none\"" } + } + + @Test + void testShouldDecryptAuthorizersWithMultipleElementsPerLine() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-populated-encrypted-multiple-per-line.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def decryptedLines = tool.decryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") + + // Assert + def passwordLines = decryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + assert passwordLines.every { it =~ ">thisIsABadPassword<" } + // Some lines were not encrypted originally so the encryption attribute would not have been updated + assert passwordLines.any { it =~ "encryption=\"none\"" } + } + + + @Test + void testDecryptAuthorizersShouldHandleCommentedElements() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-commented.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + // Act + def decryptedLines = tool.decryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Decrypted lines: \n${decryptedLines.join("\n")}") + + // Assert + + // If no encrypted properties are found, the original input text is just returned (comments and formatting in tact) + assert decryptedLines == lines + } + + @Test + void testShouldEncryptAuthorizers() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-populated.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" - String[] args = ["-n", inputPropertiesFile.path, "-l", inputLIPFile.path, "-b", bootstrapFile.path, "-i", outputLIPFile.path, "-o", outputPropertiesFile.path, "-k", KEY_HEX, "-v"] + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) - exit.checkAssertionAfterwards(new Assertion() { - public void checkAssertion() { - final List<String> updatedPropertiesLines = outputPropertiesFile.readLines() - logger.info("Updated nifi.properties:") - logger.info("\n" * 2 + updatedPropertiesLines.join("\n")) + // Act + def encryptedLines = tool.encryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") - // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) - NiFiProperties updatedProperties = new NiFiPropertiesLoader().readProtectedPropertiesFromDisk(outputPropertiesFile) - assert updatedProperties.size() >= inputProperties.size() - originalSensitiveValues.every { String key, String originalValue -> - assert updatedProperties.getProperty(key) != originalValue - } + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + assert passwordLines.every { !it.contains(">thisIsABadPassword<") } + assert passwordLines.every { it.contains(encryptionScheme) } + passwordLines.each { + String ct = (it =~ ">(.*)</property>")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } - // Check that the new NiFiProperties instance matches the output file (values still encrypted) - updatedProperties.getPropertyKeys().every { String key -> - assert updatedPropertiesLines.contains("${key}=${updatedProperties.getProperty(key)}".toString()) - } + @Test + void testShouldEncryptAuthorizersWithEmptySensitiveElements() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-populated-empty.xml" + File authorizersFile = new File(authorizersPath) - final String updatedXmlContent = outputLIPFile.text - logger.info("Updated XML content: ${updatedXmlContent}") + setupTmpDir() - // Check that the output values for sensitive properties are not the same as the original (i.e. it was encrypted) - def originalParsedXml = new XmlSlurper().parseText(originalXmlContent) - def updatedParsedXml = new XmlSlurper().parseText(updatedXmlContent) - assert originalParsedXml != updatedParsedXml - assert originalParsedXml.'**'.findAll { it.@encryption } != updatedParsedXml.'**'.findAll { - it.@encryption - } + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true - def encryptedValues = updatedParsedXml.provider.find { - it.identifier == 'ldap-provider' - }.property.findAll { - it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}" - } + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" - encryptedValues.each { - assert spp.unprotect(it.text()) == PASSWORD - } + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + def populatedPasswordLines = passwordLines.findAll { it =~ />.+</ } + assert populatedPasswordLines.every { !it.contains(">thisIsABadPassword<") } + assert populatedPasswordLines.every { it.contains(encryptionScheme) } + populatedPasswordLines.each { + String ct = (it =~ ">(.*)</property>")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testShouldEncryptAuthorizersWithMultilineElements() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-populated-multiline.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + assert passwordLines.every { !it.contains(">thisIsABadPassword<") } + assert passwordLines.every { it.contains(encryptionScheme) } + passwordLines.each { + String ct = (it =~ ">(.*)</property>")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testShouldEncryptAuthorizersWithMultipleElementsPerLine() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-populated-multiple-per-line.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + assert passwordLines.every { !it.contains(">thisIsABadPassword<") } + assert passwordLines.every { it.contains(encryptionScheme) } + passwordLines.each { + String ct = (it =~ ">(.*)</property>")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testShouldEncryptAuthorizersWithRenamedProvider() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-populated-renamed.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX + String encryptionScheme = "encryption=\"aes/gcm/${getKeyLength(KEY_HEX)}\"" + + def lines = workingFile.readLines() + logger.info("Read lines: \n${lines.join("\n")}") + assert lines.findAll { it =~ "ldap-user-group-provider" }.empty + + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX) + + // Act + def encryptedLines = tool.encryptAuthorizers(lines.join("\n")).split("\n") + logger.info("Encrypted lines: \n${encryptedLines.join("\n")}") + + // Assert + def passwordLines = encryptedLines.findAll { it =~ PASSWORD_PROP_REGEX } + assert passwordLines.size() == AUTHORIZERS_PASSWORD_LINE_COUNT + def populatedPasswordLines = passwordLines.findAll { it =~ />.+</ } + assert populatedPasswordLines.every { !it.contains(">thisIsABadPassword<") } + assert populatedPasswordLines.every { it.contains(encryptionScheme) } + populatedPasswordLines.each { + String ct = (it =~ ">(.*)</property>")[0][1] + logger.info("Cipher text: ${ct}") + assert spp.unprotect(ct) == PASSWORD + } + } + + @Test + void testEncryptAuthorizersShouldHandleCommentedElements() { + // Arrange + String authorizersPath = "src/test/resources/authorizers-commented.xml" + File authorizersFile = new File(authorizersPath) + + setupTmpDir() + + File workingFile = new File("target/tmp/tmp-authorizers.xml") + workingFile.delete() + Files.copy(authorizersFile.toPath(), workingFile.toPath()) + ConfigEncryptionTool tool = new ConfigEncryptionTool() + tool.isVerbose = true + + tool.keyHex = KEY_HEX_128 --- End diff -- This should just be `KEY_HEX` so the JCE determines which key length to use. > Support encrypted properties in authorizers.xml > ----------------------------------------------- > > Key: NIFI-4701 > URL: https://issues.apache.org/jira/browse/NIFI-4701 > Project: Apache NiFi > Issue Type: Improvement > Components: Configuration > Reporter: Kevin Doran > Assignee: Kevin Doran > Fix For: 1.5.0 > > > Since the addition of LdapUserGroupProvider (see NIFI-4059) in v1.4.0, > authorizers.xml can now contain properties for LDAP Server credentials. > This ticket is to enable properties in authorizers.xml to be encrypted, so > that the LDAP Server Manager credentials can be protected similar to > LdapProvider which is configured via login-identity-providers.xml. > The main changes are in nifi-authorizers are: > * authorizers.xsd to add an encryption attribute to Property > * to PropertyAuthorizerFactoryBean to check for that attribute and decrypt > the property value if necessary when creating the the configuration context > Additionally, support for creating an encrypted authorizers.xml, protected by > the NiFi master key, should be added to the Encrypt Tool in NiFi Toolkit. -- This message was sent by Atlassian JIRA (v6.4.14#64029)