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

feat: snowflake key auth server #34548

Merged
merged 15 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/client/src/api/DatasourcesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ class DatasourcesApi extends API {
toastMessage: undefined,
datasourceConfiguration: datasourceConfig.datasourceConfiguration && {
...datasourceConfig.datasourceConfiguration,
authentication: DatasourcesApi.cleanAuthenticationObject(
datasourceConfig.datasourceConfiguration.authentication,
),
connection: datasourceConfig.datasourceConfiguration.connection && {
...datasourceConfig.datasourceConfiguration.connection,
ssl: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.external.plugins;

import com.appsmith.external.constants.Authentication;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionRequest;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.DBAuth;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
Expand All @@ -15,32 +15,28 @@
import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.external.plugins.exceptions.SnowflakeErrorMessages;
import com.external.utils.SnowflakeKeyUtils;
import com.external.utils.SqlUtils;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import com.zaxxer.hikari.pool.HikariPool;
import lombok.extern.slf4j.Slf4j;
import net.snowflake.client.jdbc.SnowflakeBasicDataSource;
import org.bouncycastle.pkcs.PKCSException;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.security.PrivateKey;
import java.sql.*;
import java.util.*;

import static com.appsmith.external.constants.Authentication.DB_AUTH;
import static com.appsmith.external.constants.Authentication.SNOWFLAKE_KEY_PAIR_AUTH;
import static com.appsmith.external.constants.PluginConstants.PluginName.SNOWFLAKE_PLUGIN_NAME;
import static com.external.utils.ExecutionUtils.getRowsFromQueryResult;
import static com.external.utils.SnowflakeDatasourceUtils.getConnectionFromHikariConnectionPool;
Expand Down Expand Up @@ -159,58 +155,30 @@ public Mono<ActionExecutionResult> execute(
@Override
public Mono<HikariDataSource> createConnectionClient(
DatasourceConfiguration datasourceConfiguration, Properties properties) {
return Mono.fromCallable(() -> {
HikariConfig config = new HikariConfig();

config.setDriverClassName(properties.getProperty("driver_name"));

config.setMinimumIdle(
Integer.parseInt(properties.get("minimumIdle").toString()));
config.setMaximumPoolSize(Integer.parseInt(
properties.get("maximunPoolSize").toString()));

config.setInitializationFailTimeout(Long.parseLong(
properties.get("initializationFailTimeout").toString()));
config.setConnectionTimeout(Long.parseLong(
properties.get("connectionTimeoutMillis").toString()));

if (Authentication.SNOWFLAKE_KEY_PAIR_AUTH.equals(
datasourceConfiguration.getAuthentication().getAuthenticationType())) {
KeyPairAuth authentication = (KeyPairAuth) datasourceConfiguration.getAuthentication();
// Set authentication properties
if (authentication.getUsername() != null) {
config.setUsername(authentication.getUsername());
}
} else {
// Set authentication properties
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication.getUsername() != null) {
config.setUsername(authentication.getUsername());
}
if (authentication.getPassword() != null) {
config.setPassword(authentication.getPassword());
}
}

// Set up the connection URL
StringBuilder urlBuilder = new StringBuilder(
"jdbc:snowflake://" + datasourceConfiguration.getUrl() + ".snowflakecomputing.com?");
config.setJdbcUrl(urlBuilder.toString());
return getHikariConfig(datasourceConfiguration, properties)
.flatMap(config -> Mono.fromCallable(() -> {
// Set up the connection URL
String jdbcUrl = getJDBCUrl(datasourceConfiguration);
config.setJdbcUrl(jdbcUrl);

config.setDataSourceProperties(properties);
config.setDataSourceProperties(properties);

// Now create the connection pool from the configuration
HikariDataSource datasource = null;
try {
datasource = new HikariDataSource(config);
} catch (HikariPool.PoolInitializationException e) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, e.getMessage());
}
// Now create the connection pool from the configuration
HikariDataSource datasource = null;
try {
datasource = new HikariDataSource(config);
} catch (HikariPool.PoolInitializationException e) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, e.getMessage());
}
Comment on lines +170 to +173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve exception handling for Hikari pool initialization.

Consider providing more context in the exception message or using a custom exception to improve error handling.

- throw new AppsmithPluginException(
-     AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, e.getMessage());
+ throw new CustomHikariPoolException(
+     AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Error initializing Hikari pool: " + e.getMessage());
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (HikariPool.PoolInitializationException e) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, e.getMessage());
}
} catch (HikariPool.PoolInitializationException e) {
throw new CustomHikariPoolException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Error initializing Hikari pool: " + e.getMessage());
}


return datasource;
})
.subscribeOn(scheduler);
return datasource;
})
.subscribeOn(scheduler))
.onErrorMap(
AppsmithPluginException.class,
error -> new AppsmithPluginException(
Comment on lines +158 to +180
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve error handling for Hikari pool initialization.

Consider providing more context in the exception message or using a custom exception to improve error handling.

- throw new AppsmithPluginException(
-     AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, e.getMessage());
+ throw new CustomHikariPoolException(
+     AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Error initializing Hikari pool: " + e.getMessage());
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return getHikariConfig(datasourceConfiguration, properties)
.flatMap(config -> Mono.fromCallable(() -> {
// Set up the connection URL
String jdbcUrl = getJDBCUrl(datasourceConfiguration);
config.setJdbcUrl(jdbcUrl);
config.setDataSourceProperties(properties);
config.setDataSourceProperties(properties);
// Now create the connection pool from the configuration
HikariDataSource datasource = null;
try {
datasource = new HikariDataSource(config);
} catch (HikariPool.PoolInitializationException e) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, e.getMessage());
}
// Now create the connection pool from the configuration
HikariDataSource datasource = null;
try {
datasource = new HikariDataSource(config);
} catch (HikariPool.PoolInitializationException e) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, e.getMessage());
}
return datasource;
})
.subscribeOn(scheduler);
return datasource;
})
.subscribeOn(scheduler))
.onErrorMap(
AppsmithPluginException.class,
error -> new AppsmithPluginException(
return getHikariConfig(datasourceConfiguration, properties)
.flatMap(config -> Mono.fromCallable(() -> {
// Set up the connection URL
String jdbcUrl = getJDBCUrl(datasourceConfiguration);
config.setJdbcUrl(jdbcUrl);
config.setDataSourceProperties(properties);
// Now create the connection pool from the configuration
HikariDataSource datasource = null;
try {
datasource = new HikariDataSource(config);
} catch (HikariPool.PoolInitializationException e) {
throw new CustomHikariPoolException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Error initializing Hikari pool: " + e.getMessage());
}
return datasource;
})
.subscribeOn(scheduler))
.onErrorMap(
AppsmithPluginException.class,
error -> new AppsmithPluginException(

AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, error.getMessage()));
}

@Override
Expand All @@ -234,17 +202,16 @@ public Properties addPluginSpecificProperties(
@Override
public Properties addAuthParamsToConnectionConfig(
DatasourceConfiguration datasourceConfiguration, Properties properties) {

if (Authentication.SNOWFLAKE_KEY_PAIR_AUTH.equals(
datasourceConfiguration.getAuthentication().getAuthenticationType())) {
KeyPairAuth authentication = (KeyPairAuth) datasourceConfiguration.getAuthentication();
properties.setProperty("user", authentication.getUsername());
} else {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
properties.setProperty("user", authentication.getUsername());
properties.setProperty("password", authentication.getPassword());
// Only for username password auth, we need to set these properties, for others
// like key-pair auth, authentication specific properties need to be set on config itself
AuthenticationDTO authentication = datasourceConfiguration.getAuthentication();
switch (authentication.getAuthenticationType()) {
case DB_AUTH:
DBAuth dbAuth = (DBAuth) authentication;
properties.setProperty("user", dbAuth.getUsername());
properties.setProperty("password", dbAuth.getPassword());
break;
}

properties.setProperty(
"warehouse",
String.valueOf(
Expand Down Expand Up @@ -317,7 +284,7 @@ public Set<String> validateDatasource(DatasourceConfiguration datasourceConfigur
if (datasourceConfiguration.getAuthentication() == null) {
invalids.add(SnowflakeErrorMessages.DS_MISSING_AUTHENTICATION_DETAILS_ERROR_MSG);
} else {
if (Authentication.SNOWFLAKE_KEY_PAIR_AUTH.equals(
if (SNOWFLAKE_KEY_PAIR_AUTH.equals(
datasourceConfiguration.getAuthentication().getAuthenticationType())) {
KeyPairAuth authentication = (KeyPairAuth) datasourceConfiguration.getAuthentication();
if (StringUtils.isEmpty(authentication.getUsername())) {
Expand Down Expand Up @@ -491,5 +458,112 @@ public Mono<DatasourceStructure> getStructure(
})
.subscribeOn(scheduler);
}

private Mono<HikariConfig> getHikariConfig(
DatasourceConfiguration datasourceConfiguration, Properties properties) {
HikariConfig commonConfig = getCommonHikariConfig(properties);
Mono<HikariConfig> configMono = Mono.empty();

String authenticationType =
datasourceConfiguration.getAuthentication().getAuthenticationType();
if (authenticationType != null) {
switch (authenticationType) {
case DB_AUTH:
sneha122 marked this conversation as resolved.
Show resolved Hide resolved
configMono = getBasicAuthConfig(commonConfig, datasourceConfiguration);
break;
case SNOWFLAKE_KEY_PAIR_AUTH:
configMono = getKeyPairAuthConfig(commonConfig, datasourceConfiguration);
break;
default:
break;
}
}
return configMono;
}

private Mono<HikariConfig> getKeyPairAuthConfig(
HikariConfig config, DatasourceConfiguration datasourceConfiguration) {
KeyPairAuth keyPairAuthConfig = (KeyPairAuth) datasourceConfiguration.getAuthentication();
byte[] keyBytes = keyPairAuthConfig.getPrivateKey().getDecodedContent();
String passphrase = keyPairAuthConfig.getPassphrase();
return getPrivateKeyFromBase64(keyBytes, passphrase)
.flatMap(privateKey -> {
String jdbcUrl = getJDBCUrl(datasourceConfiguration);

// Prepare datasource object to be passed to hikariConfig
SnowflakeBasicDataSource ds = new SnowflakeBasicDataSource();
ds.setPrivateKey(privateKey);
ds.setUser(keyPairAuthConfig.getUsername());
ds.setUrl(jdbcUrl);
ds.setWarehouse(String.valueOf(
datasourceConfiguration.getProperties().get(0).getValue()));
ds.setDatabaseName(String.valueOf(
datasourceConfiguration.getProperties().get(1).getValue()));
ds.setRole(String.valueOf(
datasourceConfiguration.getProperties().get(3).getValue()));
ds.setSchema(String.valueOf(
datasourceConfiguration.getProperties().get(2).getValue()));
config.setDataSource(ds);

return Mono.just(config);
})
.onErrorMap(
AppsmithPluginException.class,
error -> new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, error.getMessage()));
}

private Mono<HikariConfig> getBasicAuthConfig(
HikariConfig config, DatasourceConfiguration datasourceConfiguration) {
return Mono.fromCallable(() -> {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication.getUsername() != null) {
config.setUsername(authentication.getUsername());
}
if (authentication.getPassword() != null) {
config.setPassword(authentication.getPassword());
}
return config;
});
}
sneha122 marked this conversation as resolved.
Show resolved Hide resolved

private HikariConfig getCommonHikariConfig(Properties properties) {
HikariConfig config = new HikariConfig();
config.setDriverClassName(properties.getProperty("driver_name"));

config.setMinimumIdle(Integer.parseInt(properties.get("minimumIdle").toString()));
config.setMaximumPoolSize(
Integer.parseInt(properties.get("maximunPoolSize").toString()));

config.setInitializationFailTimeout(
Long.parseLong(properties.get("initializationFailTimeout").toString()));
config.setConnectionTimeout(
Long.parseLong(properties.get("connectionTimeoutMillis").toString()));
return config;
}

private Mono<PrivateKey> getPrivateKeyFromBase64(byte[] keyBytes, String passphrase) {
try {
return Mono.just(SnowflakeKeyUtils.readEncryptedPrivateKey(keyBytes, passphrase));
} catch (AppsmithPluginException e) {
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
SnowflakeErrorMessages.DS_MISSING_PASSPHRASE_FOR_ENCRYPTED_PRIVATE_KEY));
} catch (PKCSException e) {
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
SnowflakeErrorMessages.DS_INCORRECT_PASSPHRASE_OR_PRIVATE_KEY));
} catch (Exception e) {
return Mono.error(new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
SnowflakeErrorMessages.UNABLE_TO_CREATE_CONNECTION_ERROR_MSG));
}
}

private String getJDBCUrl(DatasourceConfiguration dsConfig) {
StringBuilder urlBuilder =
new StringBuilder("jdbc:snowflake://" + dsConfig.getUrl() + ".snowflakecomputing.com?");
return urlBuilder.toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ public class SnowflakeErrorMessages extends BasePluginErrorMessages {
public static final String DS_MISSING_PASSWORD_ERROR_MSG = "Missing password for authentication.";

public static final String DS_MISSING_PRIVATE_KEY_ERROR_MSG = "Missing private key for authentication.";

public static final String DS_MISSING_PASSPHRASE_FOR_ENCRYPTED_PRIVATE_KEY =
"Passphrase is required as private key is encrypted";
public static final String DS_INCORRECT_PASSPHRASE_OR_PRIVATE_KEY = "Passphrase or private key is incorrect";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.external.utils;

import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.external.plugins.exceptions.SnowflakeErrorMessages;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;

import java.io.StringReader;
import java.security.PrivateKey;
import java.security.Security;

import static org.apache.commons.lang.StringUtils.isEmpty;

public class SnowflakeKeyUtils {
static {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}

public static PrivateKey readEncryptedPrivateKey(byte[] keyBytes, String passphrase) throws Exception {
PrivateKeyInfo privateKeyInfo = null;
String privateKeyPEM = new String(keyBytes);
PEMParser pemParser = new PEMParser(new StringReader(privateKeyPEM));
Object pemObject = pemParser.readObject();
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
// Handle the case where the private key is encrypted.
if (isEmpty(passphrase)) {
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
SnowflakeErrorMessages.DS_MISSING_PASSPHRASE_FOR_ENCRYPTED_PRIVATE_KEY);
}
PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) pemObject;
InputDecryptorProvider pkcs8Prov =
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(passphrase.toCharArray());
privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(pkcs8Prov);
} else if (pemObject instanceof PrivateKeyInfo) {
// Handle the case where the private key is unencrypted.
privateKeyInfo = (PrivateKeyInfo) pemObject;
}
pemParser.close();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
return converter.getPrivateKey(privateKeyInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public void testDatasourceWithInvalidUrl() {
DBAuth auth = new DBAuth();
auth.setUsername("test");
auth.setPassword("test");
auth.setAuthenticationType("dbAuth");
datasourceConfiguration.setAuthentication(auth);
List<Property> properties = new ArrayList<>();
properties.add(new Property("warehouse", "warehouse"));
Expand Down
Loading