diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java index 637879eb75b7..580182c2ea6b 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java @@ -34,6 +34,9 @@ public class DatasourceConfiguration implements AppsmithDomain { @JsonView({Views.Public.class, FromRequest.class}) AuthenticationDTO authentication; + @JsonView({Views.Public.class, FromRequest.class}) + TlsConfiguration tlsConfiguration; + @JsonView({Views.Public.class, FromRequest.class}) SSHConnection sshProxy; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/TlsConfiguration.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/TlsConfiguration.java new file mode 100644 index 000000000000..1c91ab2b8498 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/TlsConfiguration.java @@ -0,0 +1,36 @@ +package com.appsmith.external.models; + +import com.appsmith.external.views.FromRequest; +import com.appsmith.external.views.Views; +import com.fasterxml.jackson.annotation.JsonView; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class TlsConfiguration { + + @JsonView({Views.Public.class, FromRequest.class}) + Boolean tlsEnabled; + + @JsonView({Views.Public.class, FromRequest.class}) + Boolean verifyTlsCertificate; + + @JsonView({Views.Public.class, FromRequest.class}) + UploadedFile caCertificateFile; + + @JsonView({Views.Public.class, FromRequest.class}) + Boolean requiresClientAuth; + + @JsonView({Views.Public.class, FromRequest.class}) + UploadedFile clientCertificateFile; + + @JsonView({Views.Public.class, FromRequest.class}) + UploadedFile clientKeyFile; +} diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java index f0675168dc24..d8be2b1963fc 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java @@ -10,6 +10,7 @@ import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.RequestParamDTO; +import com.appsmith.external.models.TlsConfiguration; import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.external.plugins.exceptions.RedisErrorMessages; @@ -45,6 +46,7 @@ import java.util.stream.Collectors; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; +import static com.external.utils.RedisTLSManager.createJedisPoolWithTLS; import static org.apache.commons.lang3.StringUtils.isBlank; @Slf4j @@ -261,9 +263,16 @@ public Mono datasourceCreate(DatasourceConfiguration datasourceConfig int timeout = (int) Duration.ofSeconds(CONNECTION_TIMEOUT).toMillis(); URI uri = RedisURIUtils.getURI(datasourceConfiguration); - JedisPool jedisPool = new JedisPool(poolConfig, uri, timeout); - log.debug(Thread.currentThread().getName() + ": Created Jedis pool."); - return jedisPool; + if (datasourceConfiguration.getTlsConfiguration() == null + || !datasourceConfiguration + .getTlsConfiguration() + .getTlsEnabled()) { + JedisPool jedisPool = new JedisPool(poolConfig, uri, timeout); + log.debug(Thread.currentThread().getName() + ": Created Jedis pool."); + return jedisPool; + } else { + return createJedisPoolWithTLS(poolConfig, uri, timeout, datasourceConfiguration); + } }) .subscribeOn(scheduler); } @@ -301,6 +310,33 @@ public Set validateDatasource(DatasourceConfiguration datasourceConfigur invalids.add(RedisErrorMessages.DS_MISSING_PASSWORD_ERROR_MSG); } + TlsConfiguration tlsConfiguration = datasourceConfiguration.getTlsConfiguration(); + if (tlsConfiguration != null && tlsConfiguration.getTlsEnabled()) { + // Check for CA certificate if TLS verification is enabled + if (tlsConfiguration.getVerifyTlsCertificate() + && (tlsConfiguration.getCaCertificateFile() == null + || StringUtils.isNullOrEmpty( + tlsConfiguration.getCaCertificateFile().getBase64Content()))) { + invalids.add(RedisErrorMessages.CA_CERTIFICATE_MISSING_ERROR_MSG); + } + + // Check for client certificate and key if client authentication is required + if (tlsConfiguration.getRequiresClientAuth()) { + if (tlsConfiguration.getClientCertificateFile() == null + || StringUtils.isNullOrEmpty( + tlsConfiguration.getClientCertificateFile().getBase64Content())) { + invalids.add( + RedisErrorMessages.TLS_CLIENT_AUTH_ENABLED_BUT_CLIENT_CERTIFICATE_MISSING_ERROR_MSG); + } + + if (tlsConfiguration.getClientKeyFile() == null + || StringUtils.isNullOrEmpty( + tlsConfiguration.getClientKeyFile().getBase64Content())) { + invalids.add(RedisErrorMessages.TLS_CLIENT_AUTH_ENABLED_BUT_CLIENT_KEY_MISSING_ERROR_MSG); + } + } + } + return invalids; } diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java index f453323f31d4..58c1d7c38f46 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/exceptions/RedisErrorMessages.java @@ -29,4 +29,19 @@ public class RedisErrorMessages extends BasePluginErrorMessages { public static final String DS_MISSING_PASSWORD_ERROR_MSG = "Could not find password. Please edit the 'Password' field to provide the password."; + + /* + ************************************************************************************************************************************************ + Error messages related to TLS configuration. + ************************************************************************************************************************************************ + */ + + public static final String CA_CERTIFICATE_MISSING_ERROR_MSG = + "CA certificate is missing. Please upload the CA certificate."; + + public static final String TLS_CLIENT_AUTH_ENABLED_BUT_CLIENT_CERTIFICATE_MISSING_ERROR_MSG = + "Client authentication is enabled but the client certificate is missing. Please upload the client certificate."; + + public static final String TLS_CLIENT_AUTH_ENABLED_BUT_CLIENT_KEY_MISSING_ERROR_MSG = + "Client authentication is enabled but the client key is missing. Please upload the client key."; } diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/utils/RedisTLSManager.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/utils/RedisTLSManager.java new file mode 100644 index 000000000000..5d27464c132f --- /dev/null +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/utils/RedisTLSManager.java @@ -0,0 +1,171 @@ +package com.external.utils; + +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.TlsConfiguration; +import lombok.extern.slf4j.Slf4j; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +@Slf4j +public class RedisTLSManager { + + public static JedisPool createJedisPoolWithTLS( + JedisPoolConfig poolConfig, URI uri, int timeout, DatasourceConfiguration datasourceConfiguration) + throws Exception { + + TlsConfiguration tlsConfiguration = datasourceConfiguration.getTlsConfiguration(); + if (tlsConfiguration == null) { + throw new IllegalArgumentException("TLS configuration is missing"); + } + Boolean verifyTlsCertificate = tlsConfiguration.getVerifyTlsCertificate(); + Boolean requiresClientAuth = tlsConfiguration.getRequiresClientAuth(); + if (verifyTlsCertificate == null || requiresClientAuth == null) { + throw new IllegalArgumentException("TLS configuration flags cannot be null"); + } + SSLContext sslContext = SSLContext.getInstance("TLS"); + KeyManagerFactory keyManagerFactory = null; + try { + // Handle client authentication if required, regardless of certificate verification + if (requiresClientAuth) { + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + X509Certificate clientCert = null; + + byte[] clientCertBytes = + tlsConfiguration.getClientCertificateFile().getDecodedContent(); + + try (ByteArrayInputStream certInputStream = new ByteArrayInputStream(clientCertBytes)) { + clientCert = (X509Certificate) certificateFactory.generateCertificate(certInputStream); + + } catch (CertificateException e) { + log.error("Error occurred while parsing client certificate: " + e.getMessage()); + throw e; + } finally { + java.util.Arrays.fill(clientCertBytes, (byte) 0); + } + + PrivateKey privateKey = null; + + byte[] clientKeyBytes = tlsConfiguration.getClientKeyFile().getDecodedContent(); + + try { + privateKey = loadPrivateKey(clientKeyBytes); + } catch (Exception e) { + log.error("Error occurred while parsing private key: " + e.getMessage()); + throw e; + } finally { + java.util.Arrays.fill(clientKeyBytes, (byte) 0); + } + + // KeyStore for client authentication + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + keyStore.setKeyEntry("client-key", privateKey, null, new X509Certificate[] {clientCert}); + + keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, null); + } + + // Handle server certificate verification + TrustManager[] trustManagers; + if (verifyTlsCertificate) { + + String caCertContent = + new String(tlsConfiguration.getCaCertificateFile().getDecodedContent(), StandardCharsets.UTF_8); + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + X509Certificate caCert = (X509Certificate) + certificateFactory.generateCertificate(new ByteArrayInputStream(caCertContent.getBytes())); + + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + trustStore.setCertificateEntry("ca-cert", caCert); + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + trustManagers = trustManagerFactory.getTrustManagers(); + } else { + trustManagers = new TrustManager[] { + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + } + + // Initialize SSL context with appropriate managers + sslContext.init( + requiresClientAuth ? keyManagerFactory.getKeyManagers() : null, trustManagers, new SecureRandom()); + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + // Create and return JedisPool with TLS + JedisPool jedisPool = new JedisPool(poolConfig, uri, timeout, sslSocketFactory, null, null); + log.debug(Thread.currentThread().getName() + ": Created Jedis pool with TLS."); + return jedisPool; + } catch (CertificateException | IOException e) { + log.error("Error occurred during TLS setup (Certificate or I/O issue): {}", e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error("Unexpected error occurred while creating Jedis pool with TLS: {}", e.getMessage(), e); + throw e; + } + } + + private static PrivateKey loadPrivateKey(byte[] keyBytes) throws Exception { + byte[] decodedKey = null; + try { + String keyString = new String(keyBytes); + + String keyType = "RSA"; + if (keyString.contains("BEGIN EC PRIVATE KEY")) { + keyType = "EC"; + } + + String cleanKey = + keyString.replaceAll("-----(?:BEGIN|END)[^-]+-----", "").replaceAll("\\s", ""); + + decodedKey = Base64.getDecoder().decode(cleanKey); + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedKey); + KeyFactory kf = KeyFactory.getInstance(keyType); + return kf.generatePrivate(spec); + } catch (Exception e) { + log.error("Unexpected error while loading private key: {}", e.getMessage(), e); + throw e; + } finally { + if (decodedKey != null) { + java.util.Arrays.fill(decodedKey, (byte) 0); + } + } + } +} diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/utils/RedisURIUtils.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/utils/RedisURIUtils.java index 139ecb452d43..7f68ad8991a8 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/utils/RedisURIUtils.java +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/utils/RedisURIUtils.java @@ -3,19 +3,37 @@ import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ObjectUtils; import org.pf4j.util.StringUtils; import java.net.URI; import java.net.URISyntaxException; +@Slf4j public class RedisURIUtils { public static final Long DEFAULT_PORT = 6379L; private static final String REDIS_SCHEME = "redis://"; + private static final String REDIS_SSL_SCHEME = "rediss://"; + public static URI getURI(DatasourceConfiguration datasourceConfiguration) throws URISyntaxException { StringBuilder builder = new StringBuilder(); - builder.append(REDIS_SCHEME); + + if (datasourceConfiguration != null + && datasourceConfiguration.getTlsConfiguration() != null + && datasourceConfiguration.getTlsConfiguration().getTlsEnabled()) { + builder.append(REDIS_SSL_SCHEME); + if (datasourceConfiguration.getEndpoints() != null + && !datasourceConfiguration.getEndpoints().isEmpty()) { + Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0); + if (endpoint.getPort() != null && endpoint.getPort() == DEFAULT_PORT) { + log.warn("Using default non-TLS port {} with TLS enabled", DEFAULT_PORT); + } + } + } else { + builder.append(REDIS_SCHEME); + } String uriAuth = getUriAuth(datasourceConfiguration); builder.append(uriAuth); diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/redisPlugin/src/main/resources/form.json index d2e12db10d5f..c93e85a35cb4 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/redisPlugin/src/main/resources/form.json @@ -58,6 +58,78 @@ ] } ] + }, + { + "sectionName": "TLS Configuration", + "id": 3, + "children": [ + { + "sectionName": null, + "children": [ + { + "label": "Enable TLS", + "configProperty": "datasourceConfiguration.tlsConfiguration.tlsEnabled", + "controlType": "CHECKBOX", + "initialValue": false + }, + { + "label": "Verify TLS Certificate", + "configProperty": "datasourceConfiguration.tlsConfiguration.verifyTlsCertificate", + "controlType": "CHECKBOX", + "initialValue": false, + "hidden": { + "path": "datasourceConfiguration.tlsConfiguration.tlsEnabled", + "comparison": "NOT_EQUALS", + "value": true + } + }, + { + "label": "Upload CA Certificate", + "configProperty": "datasourceConfiguration.tlsConfiguration.caCertificateFile", + "controlType": "FILE_PICKER", + "encrypted": true, + "hidden": { + "path": "datasourceConfiguration.tlsConfiguration.tlsEnabled", + "comparison": "NOT_EQUALS", + "value": true + } + }, + { + "label": "Requires TLS Client Authentication", + "configProperty": "datasourceConfiguration.tlsConfiguration.requiresClientAuth", + "controlType": "CHECKBOX", + "initialValue": false, + "hidden": { + "path": "datasourceConfiguration.tlsConfiguration.tlsEnabled", + "comparison": "NOT_EQUALS", + "value": true + } + }, + { + "label": "Upload Client Certificate", + "configProperty": "datasourceConfiguration.tlsConfiguration.clientCertificateFile", + "controlType": "FILE_PICKER", + "encrypted": true, + "hidden": { + "path": "datasourceConfiguration.tlsConfiguration.requiresClientAuth", + "comparison": "EQUALS", + "value": false + } + }, + { + "label": "Upload Client Key", + "configProperty": "datasourceConfiguration.tlsConfiguration.clientKeyFile", + "controlType": "FILE_PICKER", + "encrypted": true, + "hidden": { + "path": "datasourceConfiguration.tlsConfiguration.requiresClientAuth", + "comparison": "EQUALS", + "value": false + } + } + ] + } + ] } ] } diff --git a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java index 9cbfbef3716c..89635d4e4126 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java +++ b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java @@ -8,6 +8,8 @@ import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.RequestParamDTO; +import com.appsmith.external.models.TlsConfiguration; +import com.appsmith.external.models.UploadedFile; import com.external.plugins.exceptions.RedisErrorMessages; import com.external.plugins.exceptions.RedisPluginError; import com.fasterxml.jackson.databind.JsonNode; @@ -26,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Set; @@ -46,6 +49,14 @@ public class RedisPluginTest { private static String host; private static Integer port; + private static final String MOCK_CA_CERTIFICATE = + "-----BEGIN CERTIFICATE-----\n" + "MIIC...mock-ca-cert...\n" + "-----END CERTIFICATE-----"; + + private static final String MOCK_CLIENT_CERTIFICATE = + "-----BEGIN CERTIFICATE-----\n" + "MIIC...mock-client-cert...\n" + "-----END CERTIFICATE-----"; + + private static final String MOCK_PRIVATE_KEY = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEv...mock-private-key...\n" + "-----END PRIVATE KEY-----"; private RedisPlugin.RedisPluginExecutor pluginExecutor = new RedisPlugin.RedisPluginExecutor(); @@ -55,6 +66,23 @@ public static void setup() { port = redis.getFirstMappedPort(); } + public static UploadedFile createUploadedFile(String fileName, String content, String mimeType) { + if (fileName == null || content == null || mimeType == null) { + throw new IllegalArgumentException("File name, content, and MIME type cannot be null"); + } + + String base64Content = + "data:" + mimeType + ";base64," + Base64.getEncoder().encodeToString(content.getBytes()); + + return new UploadedFile(fileName, base64Content); + } + + private TlsConfiguration addTLSConfiguration() { + TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setTlsEnabled(false); + return tlsConfiguration; + } + private DatasourceConfiguration createDatasourceConfiguration() { Endpoint endpoint = new Endpoint(); endpoint.setHost(host); @@ -63,6 +91,33 @@ private DatasourceConfiguration createDatasourceConfiguration() { DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + datasourceConfiguration.setTlsConfiguration(addTLSConfiguration()); + return datasourceConfiguration; + } + + private DatasourceConfiguration createDatasourceConfigurationWithTLSEnabled() { + Endpoint endpoint = new Endpoint(); + endpoint.setHost(host); + endpoint.setPort(Long.valueOf(port)); + + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + String base64CaCert = Base64.getEncoder().encodeToString(MOCK_CA_CERTIFICATE.getBytes()); + String base64ClientCert = Base64.getEncoder().encodeToString(MOCK_CLIENT_CERTIFICATE.getBytes()); + String base64PrivateKey = Base64.getEncoder().encodeToString(MOCK_PRIVATE_KEY.getBytes()); + + TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setTlsEnabled(true); + tlsConfiguration.setVerifyTlsCertificate(true); + tlsConfiguration.setCaCertificateFile( + createUploadedFile("ca-cert.crt", base64CaCert, "application/x-x509-ca-cert")); + tlsConfiguration.setRequiresClientAuth(true); + tlsConfiguration.setClientCertificateFile( + createUploadedFile("client-cert.crt", base64ClientCert, "application/x-x509-ca-cert")); + tlsConfiguration.setClientKeyFile( + createUploadedFile("client-key.key", base64PrivateKey, "application/x-pem-file")); + datasourceConfiguration.setTlsConfiguration(tlsConfiguration); return datasourceConfiguration; } @@ -79,6 +134,7 @@ public void itShouldCreateDatasource() { @Test public void itShouldValidateDatasourceWithNoEndpoints() { DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration(); + invalidDatasourceConfiguration.setTlsConfiguration(addTLSConfiguration()); assertEquals( Set.of(RedisErrorMessages.DS_MISSING_HOST_ADDRESS_ERROR_MSG), @@ -91,6 +147,7 @@ public void itShouldValidateDatasourceWithInvalidEndpoint() { Endpoint endpoint = new Endpoint(); invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + invalidDatasourceConfiguration.setTlsConfiguration(addTLSConfiguration()); assertEquals( Set.of(RedisErrorMessages.DS_MISSING_HOST_ADDRESS_ERROR_MSG), @@ -104,6 +161,7 @@ public void itShouldValidateDatasourceWithEmptyPort() { Endpoint endpoint = new Endpoint(); endpoint.setHost("test-host"); invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + invalidDatasourceConfiguration.setTlsConfiguration(addTLSConfiguration()); // Since default port is picked, set of invalids should be empty. assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of()); @@ -120,6 +178,7 @@ public void itShouldValidateDatasourceWithInvalidAuth() { invalidAuth.setUsername("username"); // skip password invalidDatasourceConfiguration.setAuthentication(invalidAuth); invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + invalidDatasourceConfiguration.setTlsConfiguration(addTLSConfiguration()); assertEquals( Set.of(RedisErrorMessages.DS_MISSING_PASSWORD_ERROR_MSG), @@ -140,6 +199,7 @@ public void itShouldValidateDatasource() { endpoint.setPort(Long.valueOf(port)); datasourceConfiguration.setAuthentication(auth); datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + datasourceConfiguration.setTlsConfiguration(addTLSConfiguration()); assertTrue(pluginExecutor.validateDatasource(datasourceConfiguration).isEmpty()); } @@ -157,6 +217,75 @@ public void itShouldTestDatasource() { .verifyComplete(); } + @Test + public void itShouldValidateDatasourceWithTlsEnabledAndMissingCACertificate() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setTlsEnabled(true); + tlsConfiguration.setVerifyTlsCertificate(true); + tlsConfiguration.setRequiresClientAuth(false); + datasourceConfiguration.setTlsConfiguration(tlsConfiguration); + + assertEquals( + Set.of(RedisErrorMessages.CA_CERTIFICATE_MISSING_ERROR_MSG), + pluginExecutor.validateDatasource(datasourceConfiguration)); + } + + @Test + public void itShouldValidateDatasourceWithTlsEnabledAndMissingClientCertificate() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setTlsEnabled(true); + tlsConfiguration.setVerifyTlsCertificate(false); + tlsConfiguration.setRequiresClientAuth(true); + tlsConfiguration.setClientKeyFile(new UploadedFile("client-key", "base64Key")); + datasourceConfiguration.setTlsConfiguration(tlsConfiguration); + assertEquals( + Set.of(RedisErrorMessages.TLS_CLIENT_AUTH_ENABLED_BUT_CLIENT_CERTIFICATE_MISSING_ERROR_MSG), + pluginExecutor.validateDatasource(datasourceConfiguration)); + } + + @Test + public void itShouldValidateDatasourceWithTlsEnabledAndMissingKey() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setTlsEnabled(true); + tlsConfiguration.setVerifyTlsCertificate(false); + tlsConfiguration.setRequiresClientAuth(true); + tlsConfiguration.setClientCertificateFile(new UploadedFile("client-cert", "base64Key")); + datasourceConfiguration.setTlsConfiguration(tlsConfiguration); + assertEquals( + Set.of(RedisErrorMessages.TLS_CLIENT_AUTH_ENABLED_BUT_CLIENT_KEY_MISSING_ERROR_MSG), + pluginExecutor.validateDatasource(datasourceConfiguration)); + } + + @Test + public void itShouldPassValidationWithTlsEnabledAndValidCertificates() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setTlsEnabled(true); + tlsConfiguration.setVerifyTlsCertificate(true); + tlsConfiguration.setRequiresClientAuth(true); + tlsConfiguration.setCaCertificateFile(new UploadedFile("ca-cert", "base64Key")); + tlsConfiguration.setClientCertificateFile(new UploadedFile("client-cert", "base64Key")); + tlsConfiguration.setClientKeyFile(new UploadedFile("client-key", "base64Key")); + datasourceConfiguration.setTlsConfiguration(tlsConfiguration); + + assertTrue(pluginExecutor.validateDatasource(datasourceConfiguration).isEmpty()); + } + + @Test + public void itShouldTestDatasourceWithTlsEnabledAndValidCertificates() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfigurationWithTLSEnabled(); + Mono datasourceTestResultMono = pluginExecutor.testDatasource(datasourceConfiguration); + + StepVerifier.create(datasourceTestResultMono) + .assertNext(datasourceTestResult -> { + assertNotNull(datasourceTestResult); + }) + .verifyComplete(); + } + @Test public void itShouldThrowErrorIfHostnameIsInvalid() {