Skip to content

Commit 431b2b8

Browse files
nishithakbhaskaranPresto CUDF CI
authored andcommitted
Add support for TLS in Redis Connector
1 parent b60cb42 commit 431b2b8

File tree

8 files changed

+199
-12
lines changed

8 files changed

+199
-12
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2249,7 +2249,7 @@
22492249
<dependency>
22502250
<groupId>redis.clients</groupId>
22512251
<artifactId>jedis</artifactId>
2252-
<version>2.6.2</version>
2252+
<version>3.8.0</version>
22532253
</dependency>
22542254

22552255
<dependency>

presto-docs/src/main/sphinx/connector/redis.rst

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Property Name Description
5151
``redis.hide-internal-columns`` Controls whether internal columns are part of the table schema or not
5252
``redis.database-index`` Redis database index
5353
``redis.password`` Redis server password
54+
``redis.user`` Redis server username
55+
``redis.tls.enabled`` Whether TLS security is enabled (defaults to ``false``)
56+
``redis.tls.truststore-path`` Path to the TLS certificate file
5457
================================= ==============================================================
5558

5659
``redis.table-names``
@@ -130,19 +133,39 @@ show up in ``DESCRIBE <table-name>`` or ``SELECT *``.
130133
This property is optional; the default is ``true``.
131134

132135
``redis.database-index``
133-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
136+
^^^^^^^^^^^^^^^^^^^^^^^^
134137

135138
The Redis database to query.
136139

137140
This property is optional; the default is ``0``.
138141

139142
``redis.password``
140-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
143+
^^^^^^^^^^^^^^^^^^
141144

142145
The password for password-protected Redis server.
143146

144147
This property is optional; the default is ``null``.
145148

149+
``redis.user``
150+
^^^^^^^^^^^^^^
151+
152+
Redis server username.
153+
154+
This property is required; there is no default.
155+
156+
``redis.tls.enabled``
157+
^^^^^^^^^^^^^^^^^^^^^
158+
159+
Enable or disable TLS security.
160+
161+
This property is optional; default is ``false``.
162+
163+
``redis.tls.truststore-path``
164+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
165+
166+
Path to the TLS certificate file.
167+
168+
This property is required if ``redis.tls.enabled`` is set to ``true``.
146169

147170
Internal Columns
148171
----------------
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
connector.name=redis
2+
redis.table-names=schema1.table1,schema1.table2
3+
redis.user=user
4+
redis.password=password
5+
redis.tls.enabled=true
6+
redis.tls.truststore-path=<path>
7+
redis.table-description-dir=<path>

presto-main/etc/config.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ plugin.bundles=\
5151
../presto-node-ttl-fetchers/pom.xml,\
5252
../presto-hive-function-namespace/pom.xml,\
5353
../presto-delta/pom.xml,\
54-
../presto-hudi/pom.xml
54+
../presto-hudi/pom.xml,\
55+
../presto-redis/pom.xml
5556

5657
presto.version=testversion
5758
node-scheduler.include-coordinator=true

presto-redis/src/main/java/com/facebook/presto/redis/RedisConnectorConfig.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public class RedisConnectorConfig
5858
*/
5959
private String redisPassword;
6060

61+
/**
62+
* user for Redis server
63+
*/
64+
private String redisUser;
65+
6166
/**
6267
* Timeout to connect to Redis.
6368
*/
@@ -88,6 +93,9 @@ public class RedisConnectorConfig
8893
*/
8994
private boolean keyPrefixSchemaTable;
9095

96+
private boolean tlsEnabled;
97+
private File truststorePath;
98+
9199
@NotNull
92100
public File getTableDescriptionDir()
93101
{
@@ -194,6 +202,11 @@ public String getRedisPassword()
194202
return redisPassword;
195203
}
196204

205+
public String getRedisUser()
206+
{
207+
return redisUser;
208+
}
209+
197210
@Config("redis.password")
198211
@ConfigSecuritySensitive
199212
public RedisConnectorConfig setRedisPassword(String redisPassword)
@@ -202,6 +215,14 @@ public RedisConnectorConfig setRedisPassword(String redisPassword)
202215
return this;
203216
}
204217

218+
@Config("redis.user")
219+
@ConfigSecuritySensitive
220+
public RedisConnectorConfig setRedisUser(String redisUser)
221+
{
222+
this.redisUser = redisUser;
223+
return this;
224+
}
225+
205226
public boolean isHideInternalColumns()
206227
{
207228
return hideInternalColumns;
@@ -236,4 +257,28 @@ private static HostAddress toHostAddress(String value)
236257
{
237258
return HostAddress.fromString(value).withDefaultPort(REDIS_DEFAULT_PORT);
238259
}
260+
261+
public boolean isTlsEnabled()
262+
{
263+
return tlsEnabled;
264+
}
265+
266+
@Config("redis.tls.enabled")
267+
public RedisConnectorConfig setTlsEnabled(boolean tlsEnabled)
268+
{
269+
this.tlsEnabled = tlsEnabled;
270+
return this;
271+
}
272+
273+
public File getTruststorePath()
274+
{
275+
return truststorePath;
276+
}
277+
278+
@Config("redis.tls.truststore-path")
279+
public RedisConnectorConfig setTruststorePath(File truststorePath)
280+
{
281+
this.truststorePath = truststorePath;
282+
return this;
283+
}
239284
}

presto-redis/src/main/java/com/facebook/presto/redis/RedisJedisManager.java

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,24 @@
2424

2525
import javax.annotation.PreDestroy;
2626
import javax.inject.Inject;
27+
import javax.net.ssl.SSLContext;
28+
import javax.net.ssl.TrustManagerFactory;
2729

30+
import java.io.IOException;
31+
import java.io.InputStream;
32+
import java.nio.file.Files;
33+
import java.security.KeyManagementException;
34+
import java.security.KeyStore;
35+
import java.security.KeyStoreException;
36+
import java.security.NoSuchAlgorithmException;
37+
import java.security.cert.CertificateException;
38+
import java.security.cert.CertificateFactory;
39+
import java.security.cert.X509Certificate;
2840
import java.util.Map;
2941

3042
import static java.lang.Math.toIntExact;
3143
import static java.util.Objects.requireNonNull;
44+
import static javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm;
3245

3346
/**
3447
* Manages connections to the Redis nodes
@@ -37,6 +50,12 @@ public class RedisJedisManager
3750
{
3851
private static final Logger log = Logger.get(RedisJedisManager.class);
3952

53+
private static final String TLS_PROTOCOL = "TLS";
54+
private static final int JEDIS_CONN_TIMEOUT = 2000;
55+
private static final int JEDIS_SO_TIMEOUT = 2000;
56+
private static final int JEDIS_MIN_IDLE_CONNECTIONS = 1;
57+
private static final int JEDIS_MAX_IDLE_CONNECTIONS = 5;
58+
4059
private final LoadingCache<HostAddress, JedisPool> jedisPoolCache;
4160

4261
private final RedisConnectorConfig redisConnectorConfig;
@@ -48,8 +67,8 @@ public class RedisJedisManager
4867
NodeManager nodeManager)
4968
{
5069
this.redisConnectorConfig = requireNonNull(redisConnectorConfig, "redisConfig is null");
51-
this.jedisPoolCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::createConsumer));
52-
this.jedisPoolConfig = new JedisPoolConfig();
70+
this.jedisPoolCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::createJedisPool));
71+
this.jedisPoolConfig = createJedisPoolConfig();
5372
}
5473

5574
@PreDestroy
@@ -76,14 +95,97 @@ public JedisPool getJedisPool(HostAddress host)
7695
return jedisPoolCache.getUnchecked(host);
7796
}
7897

79-
private JedisPool createConsumer(HostAddress host)
98+
/**
99+
* Creates a new JedisPool for the specified host.
100+
* Chooses between TLS or non-TLS configuration based on redisConnectorConfig.
101+
*/
102+
private JedisPool createJedisPool(HostAddress host)
103+
{
104+
boolean isTlsEnabled = redisConnectorConfig.isTlsEnabled();
105+
SSLContext sslContext = null;
106+
107+
if (isTlsEnabled) {
108+
KeyStore trustStore = loadTrustStore();
109+
sslContext = createSslContext(trustStore);
110+
}
111+
112+
return buildJedisPool(host, isTlsEnabled, sslContext);
113+
}
114+
/**
115+
* Creates SSLContext initialized with the given truststore.
116+
*/
117+
private SSLContext createSslContext(KeyStore trustStore)
118+
{
119+
if (trustStore == null) {
120+
throw new IllegalStateException("Truststore must not be null for TLS connections");
121+
}
122+
123+
try {
124+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(getDefaultAlgorithm());
125+
tmf.init(trustStore);
126+
127+
SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL);
128+
sslContext.init(null, tmf.getTrustManagers(), null);
129+
130+
return sslContext;
131+
}
132+
catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
133+
throw new RuntimeException("Failed to initialize SSLContext", e);
134+
}
135+
}
136+
137+
private JedisPoolConfig createJedisPoolConfig()
138+
{
139+
JedisPoolConfig config = new JedisPoolConfig();
140+
config.setMinIdle(JEDIS_MIN_IDLE_CONNECTIONS);
141+
config.setMaxTotal(JEDIS_MAX_IDLE_CONNECTIONS);
142+
return config;
143+
}
144+
/**
145+
* Loads the truststore containing Redis server certificate.
146+
* Returns null if truststore path is not configured.
147+
*/
148+
private KeyStore loadTrustStore()
80149
{
81-
log.info("Creating new JedisPool for %s", host);
82-
return new JedisPool(jedisPoolConfig,
150+
if (redisConnectorConfig.getTruststorePath() == null) {
151+
log.info("No truststore path configured, skipping TLS truststore loading");
152+
return null;
153+
}
154+
155+
try {
156+
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
157+
try (InputStream in = Files.newInputStream(redisConnectorConfig.getTruststorePath().toPath())) {
158+
trustStore.load(null, null);
159+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
160+
X509Certificate cert = (X509Certificate) cf.generateCertificate(in);
161+
trustStore.setCertificateEntry("redis-server", cert);
162+
}
163+
log.info("Loaded truststore from %s", redisConnectorConfig.getTruststorePath());
164+
return trustStore;
165+
}
166+
catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
167+
throw new RuntimeException("Failed to load truststore", e);
168+
}
169+
}
170+
171+
private JedisPool buildJedisPool(HostAddress host, boolean useTls, SSLContext sslContext)
172+
{
173+
log.info("Creating new %s JedisPool for %s", useTls ? "TLS" : "non-TLS", host);
174+
175+
return new JedisPool(
176+
jedisPoolConfig,
83177
host.getHostText(),
84178
host.getPort(),
85179
toIntExact(redisConnectorConfig.getRedisConnectTimeout().toMillis()),
180+
JEDIS_SO_TIMEOUT,
181+
JEDIS_CONN_TIMEOUT,
182+
redisConnectorConfig.getRedisUser(),
86183
redisConnectorConfig.getRedisPassword(),
87-
redisConnectorConfig.getRedisDataBaseIndex());
184+
redisConnectorConfig.getRedisDataBaseIndex(),
185+
null,
186+
useTls,
187+
useTls ? sslContext.getSocketFactory() : null,
188+
null,
189+
null);
88190
}
89191
}

presto-redis/src/main/java/com/facebook/presto/redis/RedisRecordCursor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public boolean hasUnscannedData()
118118
// no more keys are unscanned when
119119
// when redis scan command
120120
// returns 0 string cursor
121-
return (!redisCursor.getStringCursor().equals("0"));
121+
return (!redisCursor.getCursor().equals("0"));
122122
}
123123

124124
@Override
@@ -299,7 +299,7 @@ private boolean fetchKeys()
299299
case STRING: {
300300
String cursor = SCAN_POINTER_START;
301301
if (redisCursor != null) {
302-
cursor = redisCursor.getStringCursor();
302+
cursor = redisCursor.getCursor();
303303
}
304304

305305
log.debug("Scanning new Redis keys from cursor %s . %d values read so far", cursor, totalValues);

presto-redis/src/test/java/com/facebook/presto/redis/TestRedisConnectorConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public class TestRedisConnectorConfig
2626
public void testDefaults()
2727
{
2828
ConfigAssertions.assertRecordedDefaults(ConfigAssertions.recordDefaults(RedisConnectorConfig.class)
29+
.setRedisUser(null)
30+
.setTlsEnabled(false)
31+
.setTruststorePath(null)
2932
.setNodes("")
3033
.setDefaultSchema("default")
3134
.setTableNames("")
@@ -53,6 +56,9 @@ public void testExplicitPropertyMappings()
5356
.put("redis.hide-internal-columns", "false")
5457
.put("redis.connect-timeout", "10s")
5558
.put("redis.database-index", "5")
59+
.put("redis.user", "nobody")
60+
.put("redis.tls.enabled", "true")
61+
.put("redis.tls.truststore-path", "/dev/null")
5662
.put("redis.password", "secret")
5763
.build();
5864

@@ -65,6 +71,9 @@ public void testExplicitPropertyMappings()
6571
.setRedisScanCount(20)
6672
.setRedisConnectTimeout("10s")
6773
.setRedisDataBaseIndex(5)
74+
.setRedisUser("nobody")
75+
.setTlsEnabled(true)
76+
.setTruststorePath(new File("/dev/null"))
6877
.setRedisPassword("secret")
6978
.setRedisKeyDelimiter(",")
7079
.setKeyPrefixSchemaTable(true);

0 commit comments

Comments
 (0)