Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
54e19c3
snowflake custom oauth changes
Trianz-Akshay May 14, 2025
3391d2f
add unit test cases
Trianz-Akshay May 19, 2025
fa140fa
change for access_token storage in secret manager
Trianz-Akshay Jun 5, 2025
932d2d3
review comment changes
Trianz-Akshay Jun 23, 2025
0034868
review comment changes
Trianz-Akshay Jun 24, 2025
4ddf7dd
Upgrade Glue Job version to V4_0 (#2774)
MarioRial22 May 15, 2025
e309bfb
Publish maven artifacts to central (#2782)
burhan94 May 21, 2025
2473444
build(deps): bump org.elasticsearch.client:elasticsearch-rest-client …
dependabot[bot] May 21, 2025
3eecf94
build(deps): bump org.jetbrains.kotlin:kotlin-stdlib from 2.1.20 to 2…
dependabot[bot] May 23, 2025
a74a390
build(deps): bump org.jetbrains.kotlin:kotlin-stdlib-jdk8 from 2.1.20…
dependabot[bot] May 23, 2025
233ef48
build(deps): bump com.google.cloud:google-cloud-storage from 2.52.1 t…
dependabot[bot] May 23, 2025
cec9ce2
build(deps): bump io.confluent:kafka-protobuf-serializer from 7.9.0 t…
dependabot[bot] May 23, 2025
3eca8f4
build(deps): bump org.apache.kafka:kafka-clients from 7.9.0-ce to 7.9…
dependabot[bot] May 23, 2025
e2dc1a3
build(deps): bump org.jetbrains.kotlin:kotlin-reflect from 2.1.20 to …
dependabot[bot] May 23, 2025
b2e6593
build(deps): bump io.confluent:kafka-avro-serializer from 7.9.0 to 7.…
dependabot[bot] May 23, 2025
c49176b
build(deps): bump software.amazon.jsii:jsii-runtime from 1.111.0 to 1…
dependabot[bot] May 23, 2025
3c658b6
build(deps): bump software.amazon.awssdk:cloudwatchlogs from 2.31.35 …
dependabot[bot] May 23, 2025
84d7512
build(deps): bump com.teradata.jdbc:terajdbc from 20.00.00.45 to 20.0…
dependabot[bot] May 23, 2025
7e0926e
build(deps): bump aws-sdk-v2.version from 2.31.35 to 2.31.45 (#2776)
dependabot[bot] May 23, 2025
5b8f7b5
build(deps): bump com.clickhouse:clickhouse-jdbc from 0.8.5 to 0.8.6 …
dependabot[bot] May 23, 2025
f45cdd5
Feature to enhance pagination checks and abstraction (#2785)
samarsar May 23, 2025
352cdaf
build(deps): bump com.oracle.database.jdbc:ojdbc8 from 23.7.0.25.01 t…
dependabot[bot] May 27, 2025
bc9008b
build(deps): bump com.sap.cloud.db.jdbc:ngdbc from 2.24.7 to 2.24.8 (…
dependabot[bot] May 27, 2025
f849f5b
build(deps): bump software.amazon.awssdk:cloudwatchlogs from 2.31.49 …
dependabot[bot] May 27, 2025
c4facf7
build(deps): bump aws-sdk-v2.version from 2.31.49 to 2.31.50 (#2791)
dependabot[bot] May 27, 2025
e695c04
Fix big query reading record issue (#2798)
chngpe May 30, 2025
57541d8
build(deps): bump software.amazon.awssdk:cloudwatchlogs from 2.31.50 …
dependabot[bot] Jun 2, 2025
acfd5b4
build(deps): bump aws-sdk-v2.version from 2.31.50 to 2.31.54 (#2804)
dependabot[bot] Jun 2, 2025
9ad9205
build(deps): bump org.postgresql:postgresql from 42.7.5 to 42.7.6 (#2…
dependabot[bot] Jun 2, 2025
ae5655e
build(deps): bump org.junit:junit-bom from 5.12.2 to 5.13.0 (#2801)
dependabot[bot] Jun 2, 2025
26e28b9
build(deps): bump net.sf.jt400:jt400 from 21.0.3 to 21.0.4 (#2802)
dependabot[bot] Jun 2, 2025
fe1d8ad
Added fake/manual pagination for HBase connector. (#2795)
VenkatasivareddyTR Jun 3, 2025
3357fbe
Added pagination for GCS connector (#2775)
VenkatasivareddyTR Jun 5, 2025
2a7c024
Added pagination for Saphana. (#2771)
ritiktrianz Jun 5, 2025
d08e9ed
build(deps): bump net.snowflake:snowflake-jdbc from 3.24.0 to 3.24.2 …
dependabot[bot] Jun 6, 2025
e4323d8
build(deps): bump org.postgresql:postgresql from 42.7.6 to 42.7.7 in …
dependabot[bot] Jun 11, 2025
6e1a735
build(deps): bump aws-sdk-v2.version from 2.31.54 to 2.31.63 (#2830)
dependabot[bot] Jun 16, 2025
ff6d1c8
build(deps): bump software.amazon.awssdk:cloudwatchlogs from 2.31.54 …
dependabot[bot] Jun 16, 2025
4e14bd5
build(deps): bump com.squareup.wire:wire-compiler from 5.3.1 to 5.3.3…
dependabot[bot] Jun 16, 2025
27d096c
build(deps): bump fasterxml.jackson.version from 2.19.0 to 2.19.1 (#2…
dependabot[bot] Jun 16, 2025
06b6d0c
build(deps): bump org.elasticsearch.client:elasticsearch-rest-client …
dependabot[bot] Jun 16, 2025
deb5941
build(deps): bump com.squareup.wire:wire-schema from 5.3.1 to 5.3.3 (…
dependabot[bot] Jun 16, 2025
94f208f
Updating versions of bouncy castle and protobuf (#2824)
fal-bharadwaj Jun 18, 2025
23049f6
build(deps): bump net.jqwik:jqwik from 1.9.2 to 1.9.3 (#2810)
dependabot[bot] Jun 19, 2025
8333920
build(deps): bump com.squareup.wire:wire-runtime-jvm from 5.3.1 to 5.…
dependabot[bot] Jun 19, 2025
f3b6cc8
build(deps): bump com.amazonaws:aws-lambda-java-core from 1.2.3 to 1.…
dependabot[bot] Jun 19, 2025
b29da53
build(deps): bump org.junit:junit-bom from 5.13.0 to 5.13.1 (#2814)
dependabot[bot] Jun 19, 2025
df04503
build(deps): bump com.microsoft.azure:msal4j from 1.20.1 to 1.21.0 (#…
dependabot[bot] Jun 19, 2025
f653a42
build(deps): bump com.google.cloud:google-cloud-storage from 2.52.3 t…
dependabot[bot] Jun 19, 2025
5bb3a76
build(deps): bump io.lettuce:lettuce-core from 6.6.0.RELEASE to 6.7.1…
dependabot[bot] Jun 19, 2025
1c220dd
Adding project name to connectors to fix maven failures (#2847)
fal-bharadwaj Jun 23, 2025
e8611df
Updating redshift cluster node type in release tests due to deprecati…
fal-bharadwaj Jun 25, 2025
051c315
snowflake custom oauth changes
Trianz-Akshay May 14, 2025
622af16
review comment changes
Trianz-Akshay Jun 24, 2025
b27dc5f
snowflake custom oauth changes
Trianz-Akshay May 14, 2025
fde3994
review comment changes
Trianz-Akshay Jun 24, 2025
ebd2476
Merge branch 'master' into snowflake-custom-oauth
Trianz-Akshay Jun 26, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,47 @@
*/
package com.amazonaws.athena.connector.credentials;

import java.util.Map;

/**
* JDBC username and password provider.
*/
public interface CredentialsProvider
{
String USER = "user";
String PASSWORD = "password";

/**
* Retrieves credential for database.
*
* @return JDBC credential. See {@link DefaultCredentials}.
*/
DefaultCredentials getCredential();

/**
* Retrieves credential properties as a map for database connection.
*
* Default Behavior:
* The default implementation returns a map containing only the basic "user" and "password"
* properties extracted from the {@link DefaultCredentials} object returned by {@link #getCredential()}.
* This maintains backward compatibility with existing JDBC connection patterns.
*
* Extended Behavior:
* Implementations can override this method to provide additional connection properties beyond
* just username and password. This enables support for advanced authentication mechanisms.
*
* Usage:
* The returned map is directly applied to JDBC connection properties, allowing for seamless
* integration with various database drivers and authentication schemes without requiring
* custom connection factory implementations.
*
* @return Map containing credential properties for database connection. The default implementation
* returns a map with "user" and "password" keys. Overriding implementations may return
* additional properties as needed for their specific authentication requirements.
*/
default Map<String, String> getCredentialMap()
{
DefaultCredentials credential = getCredential();
return Map.of(USER, credential.getUser(), PASSWORD, credential.getPassword());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public CachableSecretsManager(SecretsManagerClient secretsManager)
this.secretsManager = secretsManager;
}

/**
* Gets the underlying SecretsManagerClient instance.
*
* @return The SecretsManagerClient instance.
*/
public SecretsManagerClient getSecretsManager()
{
return secretsManager;
}

/**
* Resolves any secrets found in the supplied string, for example: MyString${WithSecret} would have ${WithSecret}
* repalced by the corresponding value of the secret in AWS Secrets Manager with that name. If no such secret is found
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ public Connection getConnection(final CredentialsProvider credentialsProvider)
Matcher secretMatcher = SECRET_NAME_PATTERN.matcher(databaseConnectionConfig.getJdbcConnectionString());
derivedJdbcString = secretMatcher.replaceAll(Matcher.quoteReplacement(""));

jdbcProperties.put("user", credentialsProvider.getCredential().getUser());
jdbcProperties.put("password", credentialsProvider.getCredential().getPassword());
jdbcProperties.putAll(credentialsProvider.getCredentialMap());
}
else {
derivedJdbcString = databaseConnectionConfig.getJdbcConnectionString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ protected JdbcConnectionFactory getJdbcConnectionFactory()
return jdbcConnectionFactory;
}

protected DatabaseConnectionConfig getDatabaseConnectionConfig()
{
return databaseConnectionConfig;
}

protected CredentialsProvider getCredentialProvider()
{
final String secretName = databaseConnectionConfig.getSecret();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ protected JdbcConnectionFactory getJdbcConnectionFactory()
return jdbcConnectionFactory;
}

protected DatabaseConnectionConfig getDatabaseConnectionConfig()
{
return databaseConnectionConfig;
}

protected CredentialsProvider getCredentialProvider()
{
final String secretName = this.databaseConnectionConfig.getSecret();
Expand Down
1 change: 1 addition & 0 deletions athena-snowflake/athena-snowflake-connection.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Resources:
Statement:
- Action:
- secretsmanager:GetSecretValue
- secretsmanager:PutSecretValue
Effect: Allow
Resource: !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretName}*'
- Action:
Expand Down
1 change: 1 addition & 0 deletions athena-snowflake/athena-snowflake.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Resources:
- Statement:
- Action:
- secretsmanager:GetSecretValue
- secretsmanager:PutSecretValue
Effect: Allow
Resource: !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretNamePrefix}*'
Version: '2012-10-17'
Expand Down
6 changes: 6 additions & 0 deletions athena-snowflake/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
<artifactId>snowflake-jdbc</artifactId>
<version>3.24.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.json/json -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250107</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/rds -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public final class SnowflakeConstants
*/
public static final int SINGLE_SPLIT_LIMIT_COUNT = 10000;
public static final String SNOWFLAKE_QUOTE_CHARACTER = "\"";
public static final String AUTH_CODE = "auth_code";
public static final String CLIENT_ID = "client_id";
public static final String TOKEN_URL = "token_url";
public static final String REDIRECT_URI = "redirect_uri";
public static final String CLIENT_SECRET = "client_secret";

private SnowflakeConstants() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*-
* #%L
* athena-snowflake
* %%
* Copyright (C) 2019 - 2025 Amazon Web Services
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.amazonaws.athena.connectors.snowflake;

import com.amazonaws.athena.connector.credentials.CredentialsProvider;
import com.amazonaws.athena.connector.credentials.DefaultCredentials;
import com.amazonaws.athena.connector.lambda.security.CachableSecretsManager;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.utils.Validate;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
* Snowflake OAuth credentials provider that manages OAuth token lifecycle.
* This provider handles token refresh, expiration, and provides credential properties
* for Snowflake OAuth connections.
*/
public class SnowflakeCredentialsProvider implements CredentialsProvider
{
private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeCredentialsProvider.class);

public static final String ACCESS_TOKEN = "access_token";
public static final String FETCHED_AT = "fetched_at";
public static final String REFRESH_TOKEN = "refresh_token";
public static final String EXPIRES_IN = "expires_in";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String USER = "user";

private final String oauthSecretName;
private final CachableSecretsManager secretsManager;
private final ObjectMapper objectMapper;

public SnowflakeCredentialsProvider(String oauthSecretName)
{
this(oauthSecretName, SecretsManagerClient.create());
}

@VisibleForTesting
public SnowflakeCredentialsProvider(String oauthSecretName, SecretsManagerClient secretsClient)
{
this.oauthSecretName = Validate.notNull(oauthSecretName, "oauthSecretName must not be null");
this.secretsManager = new CachableSecretsManager(secretsClient);
this.objectMapper = new ObjectMapper();
}

@Override
public DefaultCredentials getCredential()
{
Map<String, String> credentialMap = getCredentialMap();
return new DefaultCredentials(
credentialMap.get(USER),
credentialMap.get(PASSWORD)
);
}

@Override
public Map<String, String> getCredentialMap()
{
try {
String secretString = secretsManager.getSecret(oauthSecretName);
Map<String, String> oauthConfig = objectMapper.readValue(secretString, Map.class);

if (oauthConfig.containsKey(SnowflakeConstants.AUTH_CODE) && !oauthConfig.get(SnowflakeConstants.AUTH_CODE).isEmpty()) {
// OAuth flow
String accessToken = fetchAccessTokenFromSecret(oauthConfig);

Map<String, String> credentialMap = new HashMap<>();
credentialMap.put(USER, oauthConfig.get(USERNAME));
credentialMap.put(PASSWORD, accessToken);
credentialMap.put("authenticator", "oauth");

return credentialMap;
}
else {
// Fallback to standard credentials
return Map.of(
USER, oauthConfig.get(USERNAME),
PASSWORD, oauthConfig.get(PASSWORD)
);
}
}
catch (Exception ex) {
throw new RuntimeException("Error retrieving Snowflake credentials: " + ex.getMessage(), ex);
}
}

private String loadTokenFromSecretsManager(Map<String, String> oauthConfig)
{
if (oauthConfig.containsKey(ACCESS_TOKEN)) {
return oauthConfig.get(ACCESS_TOKEN);
}
return null;
}

private void saveTokenToSecretsManager(JSONObject tokenJson, Map<String, String> oauthConfig)
{
// Update token related fields
tokenJson.put(FETCHED_AT, System.currentTimeMillis() / 1000);
oauthConfig.put(ACCESS_TOKEN, tokenJson.getString(ACCESS_TOKEN));
oauthConfig.put(REFRESH_TOKEN, tokenJson.getString(REFRESH_TOKEN));
oauthConfig.put(EXPIRES_IN, String.valueOf(tokenJson.getInt(EXPIRES_IN)));
oauthConfig.put(FETCHED_AT, String.valueOf(tokenJson.getLong(FETCHED_AT)));

// Save updated secret
secretsManager.getSecretsManager().putSecretValue(builder -> builder
.secretId(this.oauthSecretName)
.secretString(String.valueOf(new JSONObject(oauthConfig)))
.build());
}

private String fetchAccessTokenFromSecret(Map<String, String> oauthConfig) throws Exception
{
String accessToken;
String clientId = Validate.notNull(oauthConfig.get(SnowflakeConstants.CLIENT_ID), "Missing required property: client_id");
String tokenEndpoint = Validate.notNull(oauthConfig.get(SnowflakeConstants.TOKEN_URL), "Missing required property: token_url");
String redirectUri = Validate.notNull(oauthConfig.get(SnowflakeConstants.REDIRECT_URI), "Missing required property: redirect_uri");
String clientSecret = Validate.notNull(oauthConfig.get(SnowflakeConstants.CLIENT_SECRET), "Missing required property: client_secret");
String authCode = Validate.notNull(oauthConfig.get(SnowflakeConstants.AUTH_CODE), "Missing required property: auth_code");

accessToken = loadTokenFromSecretsManager(oauthConfig);

if (accessToken == null) {
LOGGER.debug("First time auth. Using authorization_code...");
JSONObject tokenJson = getTokenFromAuthCode(authCode, redirectUri, tokenEndpoint, clientId, clientSecret);
saveTokenToSecretsManager(tokenJson, oauthConfig);
accessToken = tokenJson.getString(ACCESS_TOKEN);
}
else {
long expiresIn = Long.parseLong(oauthConfig.get(EXPIRES_IN));
long fetchedAt = Long.parseLong(oauthConfig.getOrDefault(FETCHED_AT, String.valueOf(0L)));
long now = System.currentTimeMillis() / 1000;

if ((now - fetchedAt) < expiresIn - 60) {
LOGGER.debug("Access token still valid.");
}
else {
LOGGER.debug("Access token expired. Using refresh_token...");
JSONObject refreshed = refreshAccessToken(oauthConfig.get(REFRESH_TOKEN), tokenEndpoint, clientId, clientSecret);
refreshed.put(REFRESH_TOKEN, oauthConfig.get(REFRESH_TOKEN));
saveTokenToSecretsManager(refreshed, oauthConfig);
accessToken = refreshed.getString(ACCESS_TOKEN);
}
}
return accessToken;
}

private JSONObject getTokenFromAuthCode(String authCode, String redirectUri, String tokenEndpoint, String clientId, String clientSecret) throws Exception
{
String body = "grant_type=authorization_code"
+ "&code=" + authCode
+ "&redirect_uri=" + redirectUri;

return requestToken(body, tokenEndpoint, clientId, clientSecret);
}

private JSONObject refreshAccessToken(String refreshToken, String tokenEndpoint, String clientId, String clientSecret) throws Exception
{
String body = "grant_type=refresh_token"
+ "&refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8);

return requestToken(body, tokenEndpoint, clientId, clientSecret);
}

private JSONObject requestToken(String requestBody, String tokenEndpoint, String clientId, String clientSecret) throws Exception
{
HttpURLConnection conn = getHttpURLConnection(tokenEndpoint, clientId, clientSecret);

try (OutputStream os = conn.getOutputStream()) {
os.write(requestBody.getBytes(StandardCharsets.UTF_8));
}

int responseCode = conn.getResponseCode();
InputStream is = (responseCode >= 200 && responseCode < 300) ?
conn.getInputStream() : conn.getErrorStream();

String response = new BufferedReader(new InputStreamReader(is))
.lines()
.reduce("", (acc, line) -> acc + line);

if (responseCode != 200) {
throw new RuntimeException("Failed: " + responseCode + " - " + response);
}

JSONObject tokenJson = new JSONObject(response);
tokenJson.put(FETCHED_AT, System.currentTimeMillis() / 1000);
return tokenJson;
}

static HttpURLConnection getHttpURLConnection(String tokenEndpoint, String clientId, String clientSecret) throws IOException
{
URL url = new URL(tokenEndpoint);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

String authHeader = Base64.getEncoder()
.encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8));

conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "Basic " + authHeader);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoOutput(true);
return conn;
}
}
Loading