Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: shadow PR for external contribution #37106 [DO NOT MERGE] #37222

Closed
wants to merge 12 commits into from
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -261,9 +263,16 @@ public Mono<JedisPool> 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);
}
Expand Down Expand Up @@ -301,6 +310,33 @@ public Set<String> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading