blob: 7fdc4dd1465546554636667611af83393e4efd27 [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
import ch.qos.logback.classic.spi.LoggingEvent
import ch.qos.logback.core.AppenderBase
import org.apache.nifi.properties.ApplicationPropertiesProtector
import org.apache.nifi.properties.NiFiPropertiesLoader
import org.apache.nifi.properties.PropertyProtectionScheme
import org.apache.nifi.properties.ProtectedPropertyContext
import org.apache.nifi.properties.SensitivePropertyProvider
import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory
import org.apache.nifi.util.NiFiProperties
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.bridge.SLF4JBridgeHandler
import javax.crypto.Cipher
import java.security.Security
@RunWith(JUnit4.class)
class NiFiGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(NiFiGroovyTest.class)
private static String originalPropertiesPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
private static final String TEST_RES_PATH = NiFiGroovyTest.getClassLoader().getResource(".").toURI().getPath()
private static int getMaxKeyLength() {
return (Cipher.getMaxAllowedKeyLength("AES") > 128) ? 256 : 128
}
@BeforeClass
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
SLF4JBridgeHandler.install()
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
logger.info("Identified test resources path as ${TEST_RES_PATH}")
}
@After
void tearDown() throws Exception {
TestAppender.reset()
System.setIn(System.in)
}
@AfterClass
static void tearDownOnce() {
if (originalPropertiesPath) {
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, originalPropertiesPath)
}
}
@Test
void testInitializePropertiesShouldHandleNoBootstrapKey() throws Exception {
// Arrange
def args = [] as String[]
String plainPropertiesPath = "${TEST_RES_PATH}/NiFiProperties/conf/nifi.properties"
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, plainPropertiesPath)
// Act
NiFiProperties loadedProperties = NiFi.initializeProperties(args, NiFiGroovyTest.class.classLoader)
// Assert
assert loadedProperties.size() > 0
}
@Test
void testMainShouldHandleNoBootstrapKeyWithProtectedProperties() throws Exception {
// Arrange
def args = [] as String[]
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, "${TEST_RES_PATH}/NiFiProperties/conf/nifi_with_sensitive_properties_protected_aes_different_key.properties")
// Act
NiFi.main(args)
// Assert
assert TestAppender.events.last().getMessage() == "Failure to launch NiFi due to java.lang.IllegalArgumentException: There was an issue decrypting protected properties"
}
@Test
void testParseArgsShouldSplitCombinedArgs() throws Exception {
// Arrange
def args = ["-K filename"] as String[]
// Act
def parsedArgs = NiFi.parseArgs(args)
// Assert
assert parsedArgs.size() == 2
assert parsedArgs == args.join(" ").split(" ") as List
}
@Test
void testMainShouldHandleBadArgs() throws Exception {
// Arrange
def args = ["-K"] as String[]
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, "${TEST_RES_PATH}/NiFiProperties/conf/nifi_with_sensitive_properties_protected_aes.properties")
// Act
NiFi.main(args)
// Assert
assert TestAppender.events.collect {
it.getFormattedMessage()
}.contains("The bootstrap process passed the -K flag without a filename")
assert TestAppender.events.last().getMessage() == "Failure to launch NiFi due to java.lang.IllegalArgumentException: The bootstrap process did not provide a valid key"
}
@Test
void testMainShouldHandleMalformedBootstrapKeyFromFile() throws Exception {
// Arrange
def passwordFile = new File("${TEST_RES_PATH}/NiFiProperties/password-testMainShouldHandleMalformedBootstrapKeyFromFile.txt")
passwordFile.text = "BAD KEY"
def args = ["-K", passwordFile.absolutePath] as String[]
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, "${TEST_RES_PATH}/NiFiProperties/conf/nifi_with_sensitive_properties_protected_aes.properties")
// Act
NiFi.main(args)
// Assert
assert TestAppender.events.last().getMessage() == "Failure to launch NiFi due to java.lang.IllegalArgumentException: The bootstrap process did not provide a valid key"
}
@Test
void testInitializePropertiesShouldSetBootstrapKeyFromFile() throws Exception {
// Arrange
int currentMaxKeyLengthInBits = getMaxKeyLength()
// 64 chars of '0' for a 256 bit key; 32 chars for 128 bit
final String DIFFERENT_KEY = "0" * (currentMaxKeyLengthInBits / 4)
def passwordFile = new File("${TEST_RES_PATH}/NiFiProperties/password-testInitializePropertiesShouldSetBootstrapKeyFromFile.txt")
passwordFile.text = DIFFERENT_KEY
def args = ["-K", passwordFile.absolutePath] as String[]
String testPropertiesPath = "${TEST_RES_PATH}/NiFiProperties/conf/nifi_with_sensitive_properties_protected_aes_different_key${currentMaxKeyLengthInBits == 256 ? "" : "_128"}.properties"
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, testPropertiesPath)
def protectedNiFiProperties = new NiFiPropertiesLoader().readProtectedPropertiesFromDisk(new File(testPropertiesPath))
NiFiProperties unprocessedProperties = protectedNiFiProperties.getApplicationProperties()
def protectedKeys = getProtectedKeys(unprocessedProperties)
logger.info("Reading from raw properties file gives protected properties: ${protectedKeys}")
// Act
NiFiProperties properties = NiFi.initializeProperties(args, NiFiGroovyTest.class.classLoader)
// Assert
// Ensure that there were protected properties, they were encrypted using AES/GCM (128/256 bit key), and they were decrypted (raw value != retrieved value)
assert !hasProtectedKeys(properties)
def unprotectedProperties = decrypt(protectedNiFiProperties, DIFFERENT_KEY)
def protectedPropertyKeys = getProtectedPropertyKeys(unprocessedProperties)
protectedPropertyKeys.every { k, v ->
String rawValue = protectedNiFiProperties.getProperty(k)
logger.raw("${k} -> ${rawValue}")
String retrievedValue = properties.getProperty(k)
logger.decrypted("${k} -> ${retrievedValue}")
assert v =~ "aes/gcm"
logger.assert("${retrievedValue} != ${rawValue}")
assert retrievedValue != rawValue
String decryptedProperty = unprotectedProperties.getProperty(k)
logger.assert("${retrievedValue} == ${decryptedProperty}")
assert retrievedValue == decryptedProperty
true
}
}
private static boolean hasProtectedKeys(NiFiProperties properties) {
properties.getPropertyKeys().any { it.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) }
}
private static Map<String, String> getProtectedPropertyKeys(NiFiProperties properties) {
getProtectedKeys(properties).collectEntries { String key ->
[(key): properties.getProperty(key + ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)]
}
}
private static Set<String> getProtectedKeys(NiFiProperties properties) {
properties.getPropertyKeys().findAll { it.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) }.collect { it - ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX }
}
private static NiFiProperties decrypt(NiFiProperties encryptedProperties, String keyHex) {
SensitivePropertyProvider spp = StandardSensitivePropertyProviderFactory.withKey(keyHex)
.getProvider(PropertyProtectionScheme.AES_GCM)
def map = encryptedProperties.getPropertyKeys().collectEntries { String key ->
if (encryptedProperties.getProperty(key + ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) == spp.getIdentifierKey()) {
[(key): spp.unprotect(encryptedProperties.getProperty(key), ProtectedPropertyContext.defaultContext(key))]
} else if (!key.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)) {
[(key): encryptedProperties.getProperty(key)]
}
}
new NiFiProperties(map as Properties)
}
@Test
void testShouldValidateKeys() {
// Arrange
final List<String> VALID_KEYS = [
"0" * 64, // 256 bit keys
"ABCDEF01" * 8,
"0123" * 8, // 128 bit keys
"0123456789ABCDEFFEDCBA9876543210",
"0123456789ABCDEFFEDCBA9876543210".toLowerCase(),
]
// Act
def isValid = VALID_KEYS.collectEntries { String key -> [(key): NiFi.isHexKeyValid(key)] }
logger.info("Key validity: ${isValid}")
// Assert
assert isValid.every { k, v -> v }
}
@Test
void testShouldNotValidateInvalidKeys() {
// Arrange
final List<String> VALID_KEYS = [
"0" * 63,
"ABCDEFG1" * 8,
"0123" * 9,
"0123456789ABCDEFFEDCBA987654321",
"0123456789ABCDEF FEDCBA9876543210".toLowerCase(),
null,
"",
" "
]
// Act
def isValid = VALID_KEYS.collectEntries { String key -> [(key): NiFi.isHexKeyValid(key)] }
logger.info("Key validity: ${isValid}")
// Assert
assert isValid.every { k, v -> !v }
}
}
class TestAppender extends AppenderBase<LoggingEvent> {
static List<LoggingEvent> events = new ArrayList<>()
@Override
protected void append(LoggingEvent e) {
synchronized (events) {
events.add(e)
}
}
static void reset() {
synchronized (events) {
events.clear()
}
}
}