blob: 12f6e3a5e3fd63947044c019ec79d1f7b3e8c203 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.toolkit.encryptconfig
import org.apache.commons.lang3.SystemUtils
import org.apache.nifi.properties.PropertyProtectionScheme
import org.apache.nifi.properties.SensitivePropertyProvider
import org.apache.nifi.toolkit.encryptconfig.util.NiFiRegistryAuthorizersXmlEncryptor
import org.apache.nifi.toolkit.encryptconfig.util.NiFiRegistryIdentityProvidersXmlEncryptor
import javax.crypto.Cipher
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermission
class TestUtil {
static final String RESOURCE_REGISTRY_BOOTSTRAP_DEFAULT = absolutePathForResource('/nifi-registry/bootstrap_default.conf')
static final String RESOURCE_REGISTRY_BOOTSTRAP_NO_KEY = absolutePathForResource('/nifi-registry/bootstrap_without_root_key.conf')
static final String RESOURCE_REGISTRY_BOOTSTRAP_EMPTY_KEY = absolutePathForResource('/nifi-registry/bootstrap_with_empty_root_key.conf')
static final String RESOURCE_REGISTRY_BOOTSTRAP_KEY_128 = absolutePathForResource('/nifi-registry/bootstrap_with_root_key_128.conf')
static final String RESOURCE_REGISTRY_BOOTSTRAP_KEY_FROM_PASSWORD_128 = absolutePathForResource('/nifi-registry/bootstrap_with_root_key_from_password_128.conf')
static final String RESOURCE_REGISTRY_PROPERTIES_COMMENTED = absolutePathForResource('/nifi-registry/nifi-registry-commented.properties')
static final String RESOURCE_REGISTRY_PROPERTIES_EMPTY = absolutePathForResource('/nifi-registry/nifi-registry-empty.properties')
static final String RESOURCE_REGISTRY_PROPERTIES_POPULATED_UNPROTECTED = absolutePathForResource('/nifi-registry/nifi-registry-populated-unprotected.properties')
static final String RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_KEY_128 = absolutePathForResource('/nifi-registry/nifi-registry-populated-protected-key-128.properties')
static final String RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_KEY_256 = absolutePathForResource('/nifi-registry/nifi-registry-populated-protected-key-256.properties')
static final String RESOURCE_REGISTRY_PROPERTIES_POPULATED_PROTECTED_PASSWORD_256 = absolutePathForResource('/nifi-registry/nifi-registry-populated-protected-password-256.properties')
static final String RESOURCE_REGISTRY_AUTHORIZERS_COMMENTED = absolutePathForResource('/nifi-registry/authorizers-commented.xml')
static final String RESOURCE_REGISTRY_AUTHORIZERS_EMPTY = absolutePathForResource('/nifi-registry/authorizers-empty.xml')
static final String RESOURCE_REGISTRY_AUTHORIZERS_POPULATED_UNPROTECTED = absolutePathForResource('/nifi-registry/authorizers-populated-unprotected.xml')
static final String RESOURCE_REGISTRY_IDENTITY_PROVIDERS_COMMENTED = absolutePathForResource('/nifi-registry/identity-providers-commented.xml')
static final String RESOURCE_REGISTRY_IDENTITY_PROVIDERS_EMPTY = absolutePathForResource('/nifi-registry/identity-providers-empty.xml')
static final String RESOURCE_REGISTRY_IDENTITY_PROVIDERS_POPULATED_UNPROTECTED = absolutePathForResource('/nifi-registry/identity-providers-populated-unprotected.xml')
static final String[] RESOURCE_REGISTRY_PROPERTIES_SENSITIVE_PROPS = [
"nifi.registry.security.keystorePasswd",
"nifi.registry.security.keyPasswd",
"nifi.registry.security.truststorePasswd",
"nifi.registry.dummy.sensitive.property.1",
"nifi.registry.dummy.sensitive.property.2"
]
private static final int RESOURCE_REGISTRY_IDENTITY_PROVIDERS_PASSWORD_LINE_COUNT = 3
private static final int RESOURCE_REGISTRY_AUTHORIZERS_PASSWORD_LINE_COUNT = 3
private final String PASSWORD_PROP_REGEX = "<property[^>]* name=\".* Password\""
static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
static final String KEY_HEX_256 = KEY_HEX_128 * 2
static final String KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? KEY_HEX_256 : KEY_HEX_128
static final String PASSWORD = "thisIsABadPassword"
// From ToolUtilities.deriveKeyFromPassword("thisIsABadPassword")
static final String PASSWORD_KEY_HEX_256 = "2C576A9585DB862F5ECBEE5B4FFFCCA14B18D8365968D7081651006507AD2BDE"
static final String PASSWORD_KEY_HEX_128 = "2C576A9585DB862F5ECBEE5B4FFFCCA1"
static final String PASSWORD_KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? PASSWORD_KEY_HEX_256 : PASSWORD_KEY_HEX_128
static final String PROTECTION_SCHEME_128 = "aes/gcm/128"
static final String PROTECTION_SCHEME_256 = "aes/gcm/256"
static final String PROTECTION_SCHEME = isUnlimitedStrengthCryptoAvailable() ? PROTECTION_SCHEME_256 : PROTECTION_SCHEME_128
private static final String DEFAULT_TMP_DIR = "target/tmp/"
/**
* @return boolean indicating if the current Java Runtime Environment supports unlimited strength crypto functions
*/
static boolean isUnlimitedStrengthCryptoAvailable() {
Cipher.getMaxAllowedKeyLength("AES") > 128
}
private static absolutePathForResource(String relativeResourcePath) {
return TestUtil.class.getResource(relativeResourcePath).getPath()
}
static File setupTmpDir(String tmpDirPath = DEFAULT_TMP_DIR) {
File tmpDir = new File(tmpDirPath)
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])
tmpDir
}
static void cleanupTmpDir(String tmpDirPath = DEFAULT_TMP_DIR) {
File tmpDir = new File(tmpDirPath)
tmpDir.delete()
}
static String generateTmpFilePath() {
generateTmpFilePath("tmp_file")
}
static String generateTmpFilePath(final String tempFileSuffix) {
File tmpDir = setupTmpDir()
return "${tmpDir.getAbsolutePath()}/${UUID.randomUUID().toString()}.${tempFileSuffix}"
}
static File generateTmpFile() {
File tmpFile = new File(generateTmpFilePath())
tmpFile
}
static File generateTmpFile(final String tempFileSuffix) {
File tmpFile = new File(generateTmpFilePath(tempFileSuffix))
tmpFile
}
static String copyFileToTempFile(String filePath) {
copyFileToTempFile(filePath, "tmp_file")
}
static String copyFileToTempFile(String filePath, final String tempFileSuffix) {
File tmpFile = generateTmpFile(tempFileSuffix)
tmpFile.text = new File(filePath).text
return tmpFile.getAbsolutePath()
}
/**
* OS-agnostic method for setting file permissions. On POSIX-compliant systems, accurately sets the provided permissions. On Windows, sets the corresponding permissions for the file owner only.
*
* @param file the file to modify
* @param permissions the desired permissions
*/
static void setFilePermissions(File file, List<PosixFilePermission> permissions = []) {
if (SystemUtils.IS_OS_WINDOWS) {
file?.setReadable(permissions.contains(PosixFilePermission.OWNER_READ))
file?.setWritable(permissions.contains(PosixFilePermission.OWNER_WRITE))
file?.setExecutable(permissions.contains(PosixFilePermission.OWNER_EXECUTE))
} else {
Files.setPosixFilePermissions(file?.toPath(), permissions as Set)
}
}
/**
* Make assertions that a properties file is protected correctly given a known starting point.
*
* @param pathToOriginalUnprotectedProperties - location of the original, plaintext properties file
* @param pathToProtectedPropertiesToVerify - location of the protected properties file
* @param sensitivePropertiesToVerify - the properties that should be considered sensitive
* @param expectedProtectionSchemeToVerify - the expected protection cipher identifier
* @return true if all assertion checks pass, otherwise assertion error is thrown
*/
static boolean assertPropertiesAreProtected(
String pathToOriginalUnprotectedProperties,
String pathToProtectedPropertiesToVerify,
String[] sensitivePropertiesToVerify,
String expectedProtectionScheme = PROTECTION_SCHEME) {
Properties unprotectedProperties = new Properties()
unprotectedProperties.load(new FileReader(pathToOriginalUnprotectedProperties))
String[] populatedSensitiveProperties = sensitivePropertiesToVerify.findAll {
unprotectedProperties.getProperty(it) != null && unprotectedProperties.getProperty(it).toString().length() > 0
}
def populatedSensitivePropertiesCount = populatedSensitiveProperties.length
Properties protectedProperties = new Properties()
protectedProperties.load(new FileReader(pathToProtectedPropertiesToVerify))
// For each populated, sensitive property, one additional "*.protected" property should have been added
assert unprotectedProperties.size() + populatedSensitivePropertiesCount == protectedProperties.size()
// For each populated, sensitive property, ensure its value differs from its original value, and
// that no two protected property values match (due to IV, which is unique per-property)
Set<String> distinctValues = new HashSet<>()
populatedSensitiveProperties.every { key ->
def originalValue = unprotectedProperties.getProperty(key)
def protectedValue = protectedProperties.getProperty(key)
def protectionScheme = protectedProperties.getProperty("${key}.protected")
assert null != protectedValue
assert protectedValue.length() > 0
assert originalValue != protectedValue
assert expectedProtectionScheme == protectionScheme
assert !distinctValues.contains(protectedValue)
distinctValues.add(protectedValue)
}
return true
}
/**
* Make assertions that a NiFi Registry Authorizers XML file is protected correctly given a known starting point.
*
* @param pathToOriginalUnprotectedXml - location of the original, plaintext XML file
* @param pathToProtectedXmlToVerify - location of the protected XML file
* @param expectedProtectionScheme - expected scheme/cipher used to encrypt
* @param expectedKey - key used to encrypt
*
* @return true if all assertions pass
* @throws AssertionError if any assertion fails
*/
static boolean assertRegistryAuthorizersXmlIsProtected(
String pathToOriginalUnprotectedXml,
String pathToProtectedXmlToVerify,
String expectedProtectionScheme = PROTECTION_SCHEME,
String expectedKey = KEY_HEX) {
return assertXmlIsProtected(
pathToOriginalUnprotectedXml,
pathToProtectedXmlToVerify,
expectedProtectionScheme,
expectedKey,
{ rootNode ->
try {
rootNode.userGroupProvider.find {
it.'class'.text() == NiFiRegistryAuthorizersXmlEncryptor.LDAP_USER_GROUP_PROVIDER_CLASS
}.property.findAll {
it.@name =~ "Password"
}
} catch (Exception ignored) {
null
}
}
)
}
/**
* Make assertions that a NiFi Registry Identity Providers XML file is protected correctly given a known starting point.
*
* @param pathToOriginalUnprotectedXml - location of the original, plaintext XML file
* @param pathToProtectedXmlToVerify - location of the protected XML file
* @param expectedProtectionScheme - expected scheme/cipher used to encrypt
* @param expectedKey - key used to encrypt
*
* @return true if all assertions pass
* @throws AssertionError if any assertion fails
*/
static boolean assertRegistryIdentityProvidersXmlIsProtected(
String pathToOriginalUnprotectedXml,
String pathToProtectedXmlToVerify,
String expectedProtectionScheme = PROTECTION_SCHEME,
String expectedKey = KEY_HEX) {
return assertXmlIsProtected(
pathToOriginalUnprotectedXml,
pathToProtectedXmlToVerify,
expectedProtectionScheme,
expectedKey,
{ rootNode ->
try {
rootNode.provider.find {
it.'class'.text() == NiFiRegistryIdentityProvidersXmlEncryptor.LDAP_PROVIDER_CLASS
}.property.findAll {
it.@name =~ "Password"
}
} catch (Exception ignored) {
null
}
}
)
}
/**
* Make assertions that an XML file is protected correctly given a known starting point.
*
* @param pathToOriginalUnprotectedXml - location of the original, plaintext XML file
* @param pathToProtectedXmlToVerify - location of the protected XML file
* @param expectedProtectionScheme - expected scheme/cipher used to encrypt
* @param expectedKey - key used to encrypt
* @param callbackToGetNodesToVerify - closure that returns GPathResult[] of all sensitive nodes that
* should be protected given a GPathResult for the root of the XML document
*
* @return true if all assertions pass
* @throws AssertionError if any assertion fails
*/
static boolean assertXmlIsProtected(
String pathToOriginalUnprotectedXml,
String pathToProtectedXmlToVerify,
String expectedProtectionScheme = PROTECTION_SCHEME,
String expectedKey = KEY_HEX,
callbackToGetNodesToVerify) {
String originalUnprotectedXml = new File(pathToOriginalUnprotectedXml).text
String protectedXml = new File(pathToProtectedXmlToVerify).text
def originalDoc = new XmlParser().parseText(originalUnprotectedXml)
def protectedDoc = new XmlParser().parseText(protectedXml)
def sensitiveProperties = callbackToGetNodesToVerify(originalDoc)
assert sensitiveProperties && sensitiveProperties.size > 0 // necessary as so many key assertions are based on at least one sensitive prop
def populatedSensitiveProperties = sensitiveProperties.findAll { node ->
node.text()
}
def plaintextValues = populatedSensitiveProperties.collect {
it.text()
}
if (populatedSensitiveProperties.size() == 0) {
return assertFilesAreEqual(pathToOriginalUnprotectedXml, pathToProtectedXmlToVerify)
}
def protectedSensitiveProperties = callbackToGetNodesToVerify(protectedDoc).findAll { node ->
node.@encryption != "none" && node.@encryption != "" }
assert populatedSensitiveProperties.size() == protectedSensitiveProperties.size()
SensitivePropertyProvider spp = org.apache.nifi.properties.StandardSensitivePropertyProviderFactory.withKey(expectedKey)
.getProvider(PropertyProtectionScheme.AES_GCM)
protectedSensitiveProperties.each {
String value = it.text()
String propertyValue = value
assert it.@encryption == expectedProtectionScheme
assert !plaintextValues.contains(propertyValue)
assert plaintextValues.contains(spp.unprotect(propertyValue, org.apache.nifi.properties.ProtectedPropertyContext.defaultContext((String) it.@name)))
}
return true
}
/**
* Asserts the contents of files are equal, ignoring blank lines and starting / trailing whitespace
*
* @param pathToExpected - path to file with the expected content
* @param pathToActual - path to file with the actual content
* @return true if assertions pass
*/
static boolean assertFilesAreEqual(String pathToExpected, String pathToActual) {
List<String> expectedLines = new File(pathToExpected).readLines().findAll{
it.trim().length() > 0
}.collect{ it.trim() }
List<String> actualLines = new File(pathToActual).readLines().findAll{
it.trim().length() > 0
}.collect{ it.trim() }
return assertLinesAreEqual(expectedLines, actualLines)
}
/**
* Asserts the contents of a bootstrap.conf file match that of an an expected bootstrap.conf.
*
* @param pathToExpectedBootstrap
* @param pathToActualBootstrap
* @param includeComments - if false, comment lines in the bootstrap.conf files will be ignored
* @return true if assertions pass
*/
static boolean assertBootstrapFilesAreEqual(String pathToExpectedBootstrap, String pathToActualBootstrap, boolean includeComments) {
return assertConfOrPropertiesFilesAreEqual(pathToExpectedBootstrap, pathToActualBootstrap, includeComments)
}
/**
* Asserts the contents of a properties file match that of an an expected properties file.
*
* @param pathToExpectedProperties
* @param pathToActualProperties
* @param includeComments - if false, comment lines in the properties files will be ignored
* @return true if assertions pass
*/
static boolean assertPropertiesFilesAreEqual(String pathToExpectedProperties, String pathToActualProperties, boolean includeComments) {
return assertConfOrPropertiesFilesAreEqual(pathToExpectedProperties, pathToActualProperties, includeComments)
}
private static boolean assertConfOrPropertiesFilesAreEqual(String expected, String actual, boolean includeComments) {
List<String> expectedLines = new File(expected).readLines().findAll{
(it.trim().length() > 0 && (includeComments || !it.startsWith("#")))
}.collect{ it.trim() }
List<String> actualLines = new File(actual).readLines().findAll{
(it.trim().length() > 0 && (includeComments || !it.startsWith("#")))
}.collect{ it.trim() }
return assertLinesAreEqual(expectedLines, actualLines)
}
private static boolean assertLinesAreEqual(List<String> expectedLines, List<String> actualLines) {
assert actualLines != null
assert actualLines.size() == expectedLines.size()
assert actualLines == expectedLines
return true
}
}