From 76f33e42c675fe9ed7fdf716e8609b0aad11520b Mon Sep 17 00:00:00 2001 From: Joe Gresock Date: Wed, 7 Apr 2021 14:15:56 -0400 Subject: [PATCH] NIFI-7198: Adding optional SSL Context Service to RedisConnectionPoolService This closes #4981 Signed-off-by: David Handermann --- .../nifi-redis-extensions/pom.xml | 5 ++ .../apache/nifi/redis/util/RedisUtils.java | 28 ++++++- .../TestRedisConnectionPoolService.java | 83 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/pom.xml b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/pom.xml index 67ea977853..3bfe231344 100644 --- a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/pom.xml +++ b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/pom.xml @@ -78,5 +78,10 @@ 0.6 test + + org.apache.nifi + nifi-ssl-context-service-api + compile + diff --git a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java index 6489fcc1ea..1b5bab85e9 100644 --- a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java +++ b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java @@ -25,6 +25,8 @@ import org.apache.nifi.expression.ExpressionLanguageScope; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.redis.RedisType; +import org.apache.nifi.ssl.RestrictedSSLContextService; +import org.apache.nifi.ssl.SSLContextService; import org.apache.nifi.util.StringUtils; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConfiguration; @@ -35,6 +37,7 @@ import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import redis.clients.jedis.JedisPoolConfig; +import javax.net.ssl.SSLContext; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -234,6 +237,15 @@ public class RedisUtils { .required(true) .build(); + public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder() + .name("SSL Context Service") + .displayName("SSL Context Service") + .description("If specified, this service will be used to create an SSL Context that will be used " + + "to secure communications; if not specified, communications will not be secure") + .required(false) + .identifiesControllerService(RestrictedSSLContextService.class) + .build(); + public static final List REDIS_CONNECTION_PROPERTY_DESCRIPTORS; static { final List props = new ArrayList<>(); @@ -244,6 +256,7 @@ public class RedisUtils { props.add(RedisUtils.CLUSTER_MAX_REDIRECTS); props.add(RedisUtils.SENTINEL_MASTER); props.add(RedisUtils.PASSWORD); + props.add(RedisUtils.SSL_CONTEXT_SERVICE); props.add(RedisUtils.POOL_MAX_TOTAL); props.add(RedisUtils.POOL_MAX_IDLE); props.add(RedisUtils.POOL_MIN_IDLE); @@ -268,12 +281,23 @@ public class RedisUtils { final Integer timeout = context.getProperty(RedisUtils.COMMUNICATION_TIMEOUT).asTimePeriod(TimeUnit.MILLISECONDS).intValue(); final JedisPoolConfig poolConfig = createJedisPoolConfig(context); - final JedisClientConfiguration jedisClientConfiguration = JedisClientConfiguration.builder() + JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder() .connectTimeout(Duration.ofMillis(timeout)) .readTimeout(Duration.ofMillis(timeout)) .usePooling() .poolConfig(poolConfig) - .build(); + .and(); + + if (context.getProperty(RedisUtils.SSL_CONTEXT_SERVICE).isSet()) { + final SSLContextService sslContextService = context.getProperty(RedisUtils.SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class); + final SSLContext sslContext = sslContextService.createContext(); + builder = builder.useSsl() + .sslParameters(sslContext.getSupportedSSLParameters()) + .sslSocketFactory(sslContext.getSocketFactory()) + .and(); + } + + final JedisClientConfiguration jedisClientConfiguration = builder.build(); JedisConnectionFactory connectionFactory; if (RedisUtils.REDIS_MODE_STANDALONE.getValue().equals(redisMode)) { diff --git a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java index 2017c3b0ec..7223e65628 100644 --- a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java +++ b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java @@ -19,17 +19,37 @@ package org.apache.nifi.redis.service; import org.apache.nifi.redis.RedisConnectionPool; import org.apache.nifi.redis.util.RedisUtils; import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.ssl.RestrictedSSLContextService; +import org.apache.nifi.util.MockConfigurationContext; +import org.apache.nifi.util.MockProcessContext; +import org.apache.nifi.util.StandardProcessorTestRunner; import org.apache.nifi.util.TestRunner; import org.apache.nifi.util.TestRunners; +import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.security.GeneralSecurityException; public class TestRedisConnectionPoolService { + public static final String SSL_CONTEXT_IDENTIFIER = "ssl-context-service"; private TestRunner testRunner; private FakeRedisProcessor proc; private RedisConnectionPool redisService; + private static SSLContext sslContext; + + @BeforeClass + public static void classSetup() throws IOException, GeneralSecurityException { + sslContext = SSLContext.getDefault(); + } + @Before public void setup() throws InitializationException { proc = new FakeRedisProcessor(); @@ -39,6 +59,69 @@ public class TestRedisConnectionPoolService { testRunner.addControllerService("redis-service", redisService); } + private void enableSslContextService() throws InitializationException { + final RestrictedSSLContextService sslContextService = Mockito.mock(RestrictedSSLContextService.class); + Mockito.when(sslContextService.getIdentifier()).thenReturn(SSL_CONTEXT_IDENTIFIER); + Mockito.when(sslContextService.createContext()).thenReturn(sslContext); + testRunner.addControllerService(SSL_CONTEXT_IDENTIFIER, sslContextService); + testRunner.enableControllerService(sslContextService); + testRunner.setProperty(redisService, RedisUtils.SSL_CONTEXT_SERVICE, SSL_CONTEXT_IDENTIFIER); + } + + @Test + public void testSSLContextService() throws InitializationException { + this.setDefaultRedisProperties(); + + this.enableSslContextService(); + + testRunner.assertValid(redisService); + testRunner.enableControllerService(redisService); + + JedisConnectionFactory connectionFactory = getJedisConnectionFactory(); // Uses config from test runner + + // Verify that the client configuration will be using an SSL socket factory and SSL parameters + Assert.assertTrue(connectionFactory.getClientConfiguration().getSslSocketFactory().isPresent()); + Assert.assertTrue(connectionFactory.getClientConfiguration().getSslParameters().isPresent()); + + // Now remove the SSL context service + testRunner.disableControllerService(redisService); + testRunner.removeProperty(redisService, RedisUtils.SSL_CONTEXT_SERVICE); + testRunner.enableControllerService(redisService); + connectionFactory = getJedisConnectionFactory(); + + // Now the client configuration will not use SSL + Assert.assertFalse(connectionFactory.getClientConfiguration().getSslSocketFactory().isPresent()); + Assert.assertFalse(connectionFactory.getClientConfiguration().getSslParameters().isPresent()); + } + + private void setDefaultRedisProperties() { + testRunner.setProperty(redisService, RedisUtils.REDIS_MODE, "Standalone"); + testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, "localhost:6379"); + testRunner.setProperty(redisService, RedisUtils.DATABASE, "0"); + testRunner.setProperty(redisService, RedisUtils.COMMUNICATION_TIMEOUT, "60s"); + testRunner.setProperty(redisService, RedisUtils.CLUSTER_MAX_REDIRECTS, "5"); + testRunner.setProperty(redisService, RedisUtils.POOL_MAX_TOTAL, "1"); + testRunner.setProperty(redisService, RedisUtils.POOL_MAX_IDLE, "1"); + testRunner.setProperty(redisService, RedisUtils.POOL_MIN_IDLE, "1"); + testRunner.setProperty(redisService, RedisUtils.POOL_BLOCK_WHEN_EXHAUSTED, "true"); + testRunner.setProperty(redisService, RedisUtils.POOL_MAX_WAIT_TIME, "1s"); + testRunner.setProperty(redisService, RedisUtils.POOL_MIN_EVICTABLE_IDLE_TIME, "1s"); + testRunner.setProperty(redisService, RedisUtils.POOL_TIME_BETWEEN_EVICTION_RUNS, "1s"); + testRunner.setProperty(redisService, RedisUtils.POOL_NUM_TESTS_PER_EVICTION_RUN, "1"); + testRunner.setProperty(redisService, RedisUtils.POOL_TEST_ON_CREATE, "false"); + testRunner.setProperty(redisService, RedisUtils.POOL_TEST_ON_BORROW, "false"); + testRunner.setProperty(redisService, RedisUtils.POOL_TEST_ON_RETURN, "false"); + testRunner.setProperty(redisService, RedisUtils.POOL_TEST_WHILE_IDLE, "false"); + } + + private JedisConnectionFactory getJedisConnectionFactory() { + MockProcessContext processContext = ((StandardProcessorTestRunner) testRunner).getProcessContext(); + MockConfigurationContext configContext = new MockConfigurationContext(processContext.getControllerServices() + .get(redisService.getIdentifier()).getProperties(), processContext); + JedisConnectionFactory connectionFactory = RedisUtils.createConnectionFactory(configContext, testRunner.getLogger()); + return connectionFactory; + } + @Test public void testValidateConnectionString() { testRunner.assertNotValid(redisService);