diff --git a/parent-pom.xml b/parent-pom.xml
index 1c5ab3c2f..8642fe429 100644
--- a/parent-pom.xml
+++ b/parent-pom.xml
@@ -528,6 +528,10 @@
com.fasterxml.jackson.core
jackson-databind
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-toml
+
com.google.api
gax
diff --git a/src/main/java/net/snowflake/client/config/ConnectionParameters.java b/src/main/java/net/snowflake/client/config/ConnectionParameters.java
new file mode 100644
index 000000000..5fa97ac91
--- /dev/null
+++ b/src/main/java/net/snowflake/client/config/ConnectionParameters.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved.
+ */
+package net.snowflake.client.config;
+
+import java.util.Properties;
+import net.snowflake.client.core.SnowflakeJdbcInternalApi;
+
+@SnowflakeJdbcInternalApi
+public class ConnectionParameters {
+ private final String url;
+ private final Properties params;
+
+ public ConnectionParameters(String uri, Properties params) {
+ this.url = uri;
+ this.params = params;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public Properties getParams() {
+ return params;
+ }
+}
diff --git a/src/main/java/net/snowflake/client/config/SFConnectionConfigParser.java b/src/main/java/net/snowflake/client/config/SFConnectionConfigParser.java
new file mode 100644
index 000000000..9040fa392
--- /dev/null
+++ b/src/main/java/net/snowflake/client/config/SFConnectionConfigParser.java
@@ -0,0 +1,149 @@
+package net.snowflake.client.config;
+
+import static net.snowflake.client.jdbc.SnowflakeUtil.systemGetEnv;
+
+import com.fasterxml.jackson.dataformat.toml.TomlMapper;
+import com.google.common.base.Strings;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import net.snowflake.client.core.Constants;
+import net.snowflake.client.core.SnowflakeJdbcInternalApi;
+import net.snowflake.client.jdbc.SnowflakeSQLException;
+import net.snowflake.client.log.SFLogger;
+import net.snowflake.client.log.SFLoggerFactory;
+
+@SnowflakeJdbcInternalApi
+public class SFConnectionConfigParser {
+
+ private static final SFLogger logger = SFLoggerFactory.getLogger(SFConnectionConfigParser.class);
+ private static final TomlMapper mapper = new TomlMapper();
+ public static final String SNOWFLAKE_HOME_KEY = "SNOWFLAKE_HOME";
+ public static final String SNOWFLAKE_DIR = ".snowflake";
+ public static final String SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY =
+ "SNOWFLAKE_DEFAULT_CONNECTION_NAME";
+ public static final String DEFAULT = "default";
+ public static final String SNOWFLAKE_TOKEN_FILE_PATH = "/snowflake/session/token";
+
+ private static Map loadDefaultConnectionConfiguration(
+ String defaultConnectionName) throws SnowflakeSQLException {
+ String configDirectory =
+ Optional.ofNullable(systemGetEnv(SNOWFLAKE_HOME_KEY))
+ .orElse(Paths.get(System.getProperty("user.home"), SNOWFLAKE_DIR).toString());
+ Path configFilePath = Paths.get(configDirectory, "connections.toml");
+
+ if (Files.exists(configFilePath)) {
+ logger.debug(
+ "Reading connection parameters from file using key: {} []",
+ configFilePath,
+ defaultConnectionName);
+ Map parametersMap = readParametersMap(configFilePath);
+ Map defaultConnectionParametersMap = parametersMap.get(defaultConnectionName);
+ return defaultConnectionParametersMap;
+ } else {
+ logger.debug("Connection configuration file does not exist");
+ return new HashMap<>();
+ }
+ }
+
+ private static Map readParametersMap(Path configFilePath)
+ throws SnowflakeSQLException {
+ try {
+ File file = new File(configFilePath.toUri());
+ varifyFilePermissionSecure(configFilePath);
+ return mapper.readValue(file, Map.class);
+ } catch (IOException ex) {
+ throw new SnowflakeSQLException(ex, "Problem during reading a configuration file.");
+ }
+ }
+
+ private static void varifyFilePermissionSecure(Path configFilePath)
+ throws IOException, SnowflakeSQLException {
+ if (Constants.getOS() != Constants.OS.WINDOWS) {
+ PosixFileAttributeView posixFileAttributeView =
+ Files.getFileAttributeView(configFilePath, PosixFileAttributeView.class);
+ if (!posixFileAttributeView.readAttributes().permissions().stream()
+ .allMatch(
+ o ->
+ Arrays.asList(PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_READ)
+ .contains(o))) {
+ logger.error(
+ "Reading from file {} is not safe because of insufficient permissions", configFilePath);
+ throw new SnowflakeSQLException(
+ String.format(
+ "Reading from file %s is not safe because of insufficient permissions",
+ configFilePath));
+ }
+ }
+ }
+
+ public static ConnectionParameters buildConnectionParameters() throws SnowflakeSQLException {
+ String defaultConnectionName =
+ Optional.ofNullable(systemGetEnv(SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY)).orElse(DEFAULT);
+ Map fileConnectionConfiguration =
+ loadDefaultConnectionConfiguration(defaultConnectionName);
+
+ if (fileConnectionConfiguration != null && !fileConnectionConfiguration.isEmpty()) {
+ Properties conectionProperties = new Properties();
+ conectionProperties.putAll(fileConnectionConfiguration);
+
+ String url =
+ Optional.ofNullable(fileConnectionConfiguration.get("account"))
+ .map(ac -> createUrl(ac, fileConnectionConfiguration))
+ .orElse(null);
+ logger.debug("Url created using parameters from connection configuration file: {}", url);
+
+ if ("oauth".equals(fileConnectionConfiguration.get("authenticator"))
+ && fileConnectionConfiguration.get("token") == null) {
+ Path path =
+ Paths.get(
+ Optional.ofNullable(fileConnectionConfiguration.get("token_file_path"))
+ .orElse(SNOWFLAKE_TOKEN_FILE_PATH));
+ logger.debug("Token used in connect is read from file: {}", path);
+ try {
+ String token = new String(Files.readAllBytes(path), Charset.defaultCharset());
+ if (!token.isEmpty()) {
+ putPropertyIfNotNull(conectionProperties, "token", token.trim());
+ } else {
+ logger.warn("The token has empty value");
+ }
+ } catch (IOException ex) {
+ throw new SnowflakeSQLException(ex, "There is a problem during reading token from file");
+ }
+ }
+ return new ConnectionParameters(url, conectionProperties);
+ } else {
+ return null;
+ }
+ }
+
+ private static String createUrl(String account, Map fileConnectionConfiguration) {
+ String host = String.format("%s.snowflakecomputing.com", account);
+ String port = fileConnectionConfiguration.get("port");
+ String protocol = fileConnectionConfiguration.get("protocol");
+ if (Strings.isNullOrEmpty(port)) {
+ if ("https".equals(protocol)) {
+ port = "443";
+ } else {
+ port = "80";
+ }
+ }
+ return String.format("jdbc:snowflake://%s:%s", host, port);
+ }
+
+ private static void putPropertyIfNotNull(Properties props, Object key, Object value) {
+ if (key != null && value != null) {
+ props.put(key, value);
+ }
+ }
+}
diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java
index 6baba4a57..73f201ac2 100644
--- a/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java
+++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java
@@ -14,7 +14,12 @@
import java.sql.SQLFeatureNotSupportedException;
import java.util.List;
import java.util.Properties;
+import net.snowflake.client.config.ConnectionParameters;
+import net.snowflake.client.config.SFConnectionConfigParser;
import net.snowflake.client.core.SecurityUtil;
+import net.snowflake.client.core.SnowflakeJdbcInternalApi;
+import net.snowflake.client.log.SFLogger;
+import net.snowflake.client.log.SFLoggerFactory;
import net.snowflake.common.core.ResourceBundleManager;
import net.snowflake.common.core.SqlState;
@@ -26,6 +31,8 @@
* loading
*/
public class SnowflakeDriver implements Driver {
+ private static final SFLogger logger = SFLoggerFactory.getLogger(SnowflakeDriver.class);
+ public static final String AUTO_CONNECTION_STRING_PREFIX = "jdbc:snowflake:auto";
static SnowflakeDriver INSTANCE;
public static final Properties EMPTY_PROPERTIES = new Properties();
@@ -200,18 +207,52 @@ public boolean acceptsURL(String url) {
*/
@Override
public Connection connect(String url, Properties info) throws SQLException {
- if (url == null) {
+ ConnectionParameters connectionParameters =
+ overrideByFileConnectionParametersIfAutoConfiguration(url, info);
+
+ if (connectionParameters.getUrl() == null) {
// expected return format per the JDBC spec for java.sql.Driver#connect()
throw new SnowflakeSQLException("Unable to connect to url of 'null'.");
}
- if (!SnowflakeConnectString.hasSupportedPrefix(url)) {
+ if (!SnowflakeConnectString.hasSupportedPrefix(connectionParameters.getUrl())) {
return null; // expected return format per the JDBC spec for java.sql.Driver#connect()
}
- SnowflakeConnectString conStr = SnowflakeConnectString.parse(url, info);
+ SnowflakeConnectString conStr =
+ SnowflakeConnectString.parse(
+ connectionParameters.getUrl(), connectionParameters.getParams());
if (!conStr.isValid()) {
throw new SnowflakeSQLException("Connection string is invalid. Unable to parse.");
}
- return new SnowflakeConnectionV1(url, info);
+ return new SnowflakeConnectionV1(
+ connectionParameters.getUrl(), connectionParameters.getParams());
+ }
+
+ private static ConnectionParameters overrideByFileConnectionParametersIfAutoConfiguration(
+ String url, Properties info) throws SnowflakeSQLException {
+ if (url != null && url.contains(AUTO_CONNECTION_STRING_PREFIX)) {
+ // Connect using connection configuration file
+ ConnectionParameters connectionParameters =
+ SFConnectionConfigParser.buildConnectionParameters();
+ if (connectionParameters == null) {
+ throw new SnowflakeSQLException(
+ "Unavailable connection configuration parameters expected for auto configuration using file");
+ }
+ return connectionParameters;
+ } else {
+ return new ConnectionParameters(url, info);
+ }
+ }
+
+ /**
+ * Connect method using connection configuration file
+ *
+ * @return connection
+ * @throws SQLException if failed to create a snowflake connection
+ */
+ @SnowflakeJdbcInternalApi
+ public Connection connect() throws SQLException {
+ logger.debug("Execute internal method connect() without parameters");
+ return connect(AUTO_CONNECTION_STRING_PREFIX, null);
}
@Override
diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java
index 660e83134..a88829ec6 100644
--- a/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java
+++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeSQLException.java
@@ -172,6 +172,10 @@ public SnowflakeSQLException(String reason) {
super(reason);
}
+ public SnowflakeSQLException(Throwable ex, String message) {
+ super(message, ex);
+ }
+
public String getQueryId() {
return queryId;
}
diff --git a/src/test/java/net/snowflake/client/RunningNotOnWin.java b/src/test/java/net/snowflake/client/RunningNotOnWin.java
new file mode 100644
index 000000000..ce5cdf7d1
--- /dev/null
+++ b/src/test/java/net/snowflake/client/RunningNotOnWin.java
@@ -0,0 +1,9 @@
+package net.snowflake.client;
+
+import net.snowflake.client.core.Constants;
+
+public class RunningNotOnWin implements ConditionalIgnoreRule.IgnoreCondition {
+ public boolean isSatisfied() {
+ return Constants.getOS() != Constants.OS.WINDOWS;
+ }
+}
diff --git a/src/test/java/net/snowflake/client/config/SFConnectionConfigParserTest.java b/src/test/java/net/snowflake/client/config/SFConnectionConfigParserTest.java
new file mode 100644
index 000000000..e68e68fa0
--- /dev/null
+++ b/src/test/java/net/snowflake/client/config/SFConnectionConfigParserTest.java
@@ -0,0 +1,133 @@
+package net.snowflake.client.config;
+
+import static net.snowflake.client.config.SFConnectionConfigParser.SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY;
+import static net.snowflake.client.config.SFConnectionConfigParser.SNOWFLAKE_HOME_KEY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+
+import com.fasterxml.jackson.dataformat.toml.TomlMapper;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import net.snowflake.client.RunningNotOnLinuxMac;
+import net.snowflake.client.core.Constants;
+import net.snowflake.client.jdbc.SnowflakeSQLException;
+import net.snowflake.client.jdbc.SnowflakeUtil;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SFConnectionConfigParserTest {
+
+ private Path tempPath = null;
+ private TomlMapper tomlMapper = new TomlMapper();
+
+ @Before
+ public void setUp() throws IOException {
+ tempPath = Files.createTempDirectory(".snowflake");
+ }
+
+ @After
+ public void close() throws IOException {
+ SnowflakeUtil.systemUnsetEnv(SNOWFLAKE_HOME_KEY);
+ SnowflakeUtil.systemUnsetEnv(SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY);
+ Files.walk(tempPath).map(Path::toFile).forEach(File::delete);
+ Files.delete(tempPath);
+ }
+
+ @Test
+ public void testLoadSFConnectionConfigWrongConfigurationName()
+ throws SnowflakeSQLException, IOException {
+ SnowflakeUtil.systemSetEnv(SNOWFLAKE_HOME_KEY, tempPath.toString());
+ SnowflakeUtil.systemSetEnv(SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY, "unknown");
+ prepareConnectionConfigurationTomlFile(null, true);
+ ConnectionParameters connectionParameters =
+ SFConnectionConfigParser.buildConnectionParameters();
+ assertNull(connectionParameters);
+ }
+
+ @Test
+ public void testLoadSFConnectionConfigInValidPath() throws SnowflakeSQLException, IOException {
+ SnowflakeUtil.systemSetEnv(SNOWFLAKE_HOME_KEY, Paths.get("unknownPath").toString());
+ prepareConnectionConfigurationTomlFile(null, true);
+ assertNull(SFConnectionConfigParser.buildConnectionParameters());
+ }
+
+ @Test
+ public void testLoadSFConnectionConfigWithTokenFromFile()
+ throws SnowflakeSQLException, IOException {
+ SnowflakeUtil.systemSetEnv(SNOWFLAKE_HOME_KEY, tempPath.toString());
+ SnowflakeUtil.systemSetEnv(SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY, "default");
+ File tokenFile = new File(Paths.get(tempPath.toString(), "token").toUri());
+ prepareConnectionConfigurationTomlFile(
+ Collections.singletonMap("token_file_path", tokenFile.toString()), true);
+
+ ConnectionParameters data = SFConnectionConfigParser.buildConnectionParameters();
+ assertNotNull(data);
+ assertEquals(tokenFile.toString(), data.getParams().get("token_file_path"));
+ assertEquals("testToken", data.getParams().get("token"));
+ }
+
+ @Test
+ public void testThrowErrorWhenWrongPermissionsForTokenFile() throws IOException {
+ SnowflakeUtil.systemSetEnv(SNOWFLAKE_HOME_KEY, tempPath.toString());
+ File tokenFile = new File(Paths.get(tempPath.toString(), "token").toUri());
+ prepareConnectionConfigurationTomlFile(
+ Collections.singletonMap("token_file_path", tokenFile.toString()), false);
+ assumeFalse(RunningNotOnLinuxMac.isNotRunningOnLinuxMac());
+ assertThrows(
+ SnowflakeSQLException.class, () -> SFConnectionConfigParser.buildConnectionParameters());
+ }
+
+ private void prepareConnectionConfigurationTomlFile(
+ Map moreParameters, boolean onlyUserPermission) throws IOException {
+ Path path = Paths.get(tempPath.toString(), "connections.toml");
+ Path filePath = createFilePathWithPermission(path, onlyUserPermission);
+ File file = filePath.toFile();
+
+ Map configuration = new HashMap();
+ Map configurationParams = new HashMap();
+ configurationParams.put("account", "snowaccount.us-west-2.aws");
+ configurationParams.put("user", "user1");
+ configurationParams.put("token", "testToken");
+ configurationParams.put("port", "443");
+
+ if (moreParameters != null) {
+ moreParameters.forEach((k, v) -> configurationParams.put(k, v));
+ }
+ configuration.put("default", configurationParams);
+ tomlMapper.writeValue(file, configuration);
+
+ if (configurationParams.containsKey("token_file_path")) {
+ Path tokenFilePath =
+ createFilePathWithPermission(
+ Paths.get(configurationParams.get("token_file_path").toString()), onlyUserPermission);
+ Files.write(tokenFilePath, "token_from_file".getBytes());
+ }
+ }
+
+ private Path createFilePathWithPermission(Path path, boolean onlyUserPermission)
+ throws IOException {
+ if (Constants.getOS() != Constants.OS.WINDOWS) {
+ FileAttribute> fileAttribute =
+ onlyUserPermission
+ ? PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"))
+ : PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrw----"));
+ return Files.createFile(path, fileAttribute);
+ } else {
+ return Files.createFile(path);
+ }
+ }
+}
diff --git a/src/test/java/net/snowflake/client/jdbc/FileConnectionConfigurationLatestIT.java b/src/test/java/net/snowflake/client/jdbc/FileConnectionConfigurationLatestIT.java
new file mode 100644
index 000000000..734446c92
--- /dev/null
+++ b/src/test/java/net/snowflake/client/jdbc/FileConnectionConfigurationLatestIT.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2012-2020 Snowflake Computing Inc. All right reserved.
+ */
+package net.snowflake.client.jdbc;
+
+import static net.snowflake.client.config.SFConnectionConfigParser.SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/** This test could be run only on environment where file connection.toml is configured */
+@Ignore
+public class FileConnectionConfigurationLatestIT {
+
+ @After
+ public void cleanUp() {
+ SnowflakeUtil.systemUnsetEnv(SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY);
+ }
+
+ @Test
+ public void testThrowExceptionIfConfigurationDoesNotExist() {
+ SnowflakeUtil.systemSetEnv("SNOWFLAKE_DEFAULT_CONNECTION_NAME", "non-existent");
+ Assert.assertThrows(SnowflakeSQLException.class, () -> SnowflakeDriver.INSTANCE.connect());
+ }
+
+ @Test
+ public void testSimpleConnectionUsingFileConfigurationToken() throws SQLException {
+ verifyConnetionToSnowflake("aws-oauth");
+ }
+
+ @Test
+ public void testSimpleConnectionUsingFileConfigurationTokenFromFile() throws SQLException {
+ verifyConnetionToSnowflake("aws-oauth-file");
+ }
+
+ private static void verifyConnetionToSnowflake(String connectionName) throws SQLException {
+ SnowflakeUtil.systemSetEnv(SNOWFLAKE_DEFAULT_CONNECTION_NAME_KEY, connectionName);
+ try (Connection con =
+ DriverManager.getConnection(SnowflakeDriver.AUTO_CONNECTION_STRING_PREFIX, null);
+ Statement statement = con.createStatement();
+ ResultSet resultSet = statement.executeQuery("show parameters")) {
+ Assert.assertTrue(resultSet.next());
+ }
+ }
+}
diff --git a/thin_public_pom.xml b/thin_public_pom.xml
index 239e31e34..e15a4e3c4 100644
--- a/thin_public_pom.xml
+++ b/thin_public_pom.xml
@@ -140,6 +140,10 @@
com.fasterxml.jackson.core
jackson-databind
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-toml
+
com.google.api
gax