Skip to content

Commit

Permalink
SNOW-1454054 - Read connection configuration from file. (#1780)
Browse files Browse the repository at this point in the history
* SNOW-1454054 - Read connection configuration from file.
  • Loading branch information
sfc-gh-pmotacki authored Jun 24, 2024
1 parent 6d11e4f commit ccee1b1
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 4 deletions.
4 changes: 4 additions & 0 deletions parent-pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-toml</artifactId>
</dependency>
<dependency>
<groupId>com.google.api</groupId>
<artifactId>gax</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, Map> parametersMap = readParametersMap(configFilePath);
Map<String, String> defaultConnectionParametersMap = parametersMap.get(defaultConnectionName);
return defaultConnectionParametersMap;
} else {
logger.debug("Connection configuration file does not exist");
return new HashMap<>();
}
}

private static Map<String, 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<String, String> 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<String, String> 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);
}
}
}
49 changes: 45 additions & 4 deletions src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ public SnowflakeSQLException(String reason) {
super(reason);
}

public SnowflakeSQLException(Throwable ex, String message) {
super(message, ex);
}

public String getQueryId() {
return queryId;
}
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/net/snowflake/client/RunningNotOnWin.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit ccee1b1

Please sign in to comment.