Skip to content

Commit 346cd16

Browse files
SNOW-2204396: Add platform detection results to CLIENT_ENVIRONMENT (#2351)
1 parent 2fef64a commit 346cd16

25 files changed

Lines changed: 1796 additions & 13 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
# Changelog
44
- v3.27.1-SNAPSHOT
5+
- Added platform detection on login to set PLATFORM metric in CLIENT_ENVIRONMENT
6+
57
- v3.27.0
68
- Added the `changelog.yml` GitHub workflow to ensure changelog is updated on release PRs.
79
- Added HTTP 307 & 308 retries in case of internal IP redirects

src/main/java/net/snowflake/client/core/SFLoginInput.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public class SFLoginInput {
5151
private String privateKeyBase64;
5252
private String privateKeyPwd;
5353
private String inFlightCtx; // Opaque string sent for Snowsight account activation
54+
private int platformDetectionTimeoutMs = 200; // Default 200ms timeout for platform detection
55+
private boolean disablePlatformDetection = false; // Default false - platform detection enabled
5456

5557
private SFOauthLoginInput oauthLoginInput;
5658

@@ -637,4 +639,22 @@ public void setBrowserHandler(
637639
SessionUtilExternalBrowser.AuthExternalBrowserHandlers browserHandler) {
638640
this.browserHandler = browserHandler;
639641
}
642+
643+
public int getPlatformDetectionTimeoutMs() {
644+
return platformDetectionTimeoutMs;
645+
}
646+
647+
public SFLoginInput setPlatformDetectionTimeoutMs(int platformDetectionTimeoutMs) {
648+
this.platformDetectionTimeoutMs = platformDetectionTimeoutMs;
649+
return this;
650+
}
651+
652+
public boolean isDisablePlatformDetection() {
653+
return disablePlatformDetection;
654+
}
655+
656+
public SFLoginInput setDisablePlatformDetection(boolean disablePlatformDetection) {
657+
this.disablePlatformDetection = disablePlatformDetection;
658+
return this;
659+
}
640660
}

src/main/java/net/snowflake/client/core/SFSession.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ public class SFSession extends SFBaseSession {
143143
*/
144144
private int retryTimeout = 300;
145145

146+
private int defaultPlatformDetectionTimeoutMs = 200;
147+
146148
private boolean enableClientStoreTemporaryCredential = true;
147149
private boolean enableClientRequestMfaToken = true;
148150

@@ -794,7 +796,16 @@ public synchronized void open() throws SFException, SnowflakeSQLException {
794796
: false)
795797
.setEnableClientStoreTemporaryCredential(enableClientStoreTemporaryCredential)
796798
.setEnableClientRequestMfaToken(enableClientRequestMfaToken)
797-
.setBrowserResponseTimeout(browserResponseTimeout);
799+
.setBrowserResponseTimeout(browserResponseTimeout)
800+
.setPlatformDetectionTimeoutMs(
801+
connectionPropertiesMap.get(SFSessionProperty.PLATFORM_DETECTION_TIMEOUT_MS) != null
802+
? (int) connectionPropertiesMap.get(SFSessionProperty.PLATFORM_DETECTION_TIMEOUT_MS)
803+
: defaultPlatformDetectionTimeoutMs)
804+
.setDisablePlatformDetection(
805+
connectionPropertiesMap.get(SFSessionProperty.DISABLE_PLATFORM_DETECTION) != null
806+
? getBooleanValue(
807+
connectionPropertiesMap.get(SFSessionProperty.DISABLE_PLATFORM_DETECTION))
808+
: false); // Default to false (platform detection enabled)
798809

799810
logger.info(
800811
"Connecting to {} Snowflake domain",

src/main/java/net/snowflake/client/core/SFSessionProperty.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ public enum SFSessionProperty {
153153

154154
CERT_REVOCATION_CHECK_MODE("CERT_REVOCATION_CHECK_MODE", false, String.class),
155155

156-
ALLOW_CERTIFICATES_WITHOUT_CRL_URL("ALLOW_CERTIFICATES_WITHOUT_CRL_URL", false, Boolean.class);
156+
ALLOW_CERTIFICATES_WITHOUT_CRL_URL("ALLOW_CERTIFICATES_WITHOUT_CRL_URL", false, Boolean.class),
157+
158+
PLATFORM_DETECTION_TIMEOUT_MS("platformDetectionTimeoutMs", false, Integer.class),
159+
160+
DISABLE_PLATFORM_DETECTION("disablePlatformDetection", false, Boolean.class);
157161

158162
// property key in string
159163
private String propertyKey;

src/main/java/net/snowflake/client/core/SessionUtil.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.HashMap;
2121
import java.util.HashSet;
2222
import java.util.Iterator;
23+
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Set;
2526
import java.util.stream.Collectors;
@@ -54,6 +55,7 @@
5455
import net.snowflake.client.log.ArgSupplier;
5556
import net.snowflake.client.log.SFLogger;
5657
import net.snowflake.client.log.SFLoggerFactory;
58+
import net.snowflake.client.util.PlatformDetector;
5759
import net.snowflake.client.util.SecretDetector;
5860
import net.snowflake.client.util.Stopwatch;
5961
import net.snowflake.client.util.ThrowingFunction;
@@ -1146,6 +1148,23 @@ static Map<String, Object> createClientEnvironmentInfo(
11461148

11471149
clientEnv.put("JDBC_JAR_NAME", SnowflakeDriver.getJdbcJarname());
11481150

1151+
// Add platform detection (if not disabled)
1152+
if (!loginInput.isDisablePlatformDetection()) {
1153+
try {
1154+
PlatformDetector platformDetector = new PlatformDetector();
1155+
AwsAttestationService awsAttestationService = new AwsAttestationService();
1156+
List<String> detectedPlatforms =
1157+
platformDetector.detectPlatforms(
1158+
loginInput.getPlatformDetectionTimeoutMs(), awsAttestationService);
1159+
clientEnv.put("PLATFORM", detectedPlatforms);
1160+
} catch (Exception e) {
1161+
logger.debug("Platform detection failed: {}", e.getMessage());
1162+
// Continue without platform information
1163+
}
1164+
} else {
1165+
logger.debug("Platform detection is disabled");
1166+
}
1167+
11491168
// OAuth metrics data
11501169
if (authenticatorType == AuthenticatorType.OAUTH
11511170
&& loginInput.getOriginalAuthenticator() != null) {

src/main/java/net/snowflake/client/core/auth/wif/AwsAttestationService.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ void initializeSignerRegion() {
2929
aws4Signer.setRegionName(getAWSRegion());
3030
}
3131

32-
AWSCredentials getAWSCredentials() {
33-
return DefaultAWSCredentialsProviderChain.getInstance().getCredentials();
32+
public AWSCredentials getAWSCredentials() {
33+
try {
34+
return DefaultAWSCredentialsProviderChain.getInstance().getCredentials();
35+
} catch (Exception e) {
36+
logger.debug("Failed to retrieve AWS credentials: {}", e.getMessage());
37+
return null;
38+
}
3439
}
3540

3641
String getAWSRegion() {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package net.snowflake.client.core.auth.wif;
2+
3+
import com.amazonaws.auth.AWSCredentials;
4+
import java.io.IOException;
5+
import net.snowflake.client.core.HttpClientSettingsKey;
6+
import net.snowflake.client.core.HttpUtil;
7+
import net.snowflake.client.core.OCSPMode;
8+
import net.snowflake.client.core.SnowflakeJdbcInternalApi;
9+
import net.snowflake.client.jdbc.SnowflakeSQLException;
10+
import net.snowflake.client.log.SFLogger;
11+
import net.snowflake.client.log.SFLoggerFactory;
12+
import org.apache.http.client.methods.HttpRequestBase;
13+
14+
@SnowflakeJdbcInternalApi
15+
public class PlatformDetectionUtil {
16+
17+
private static final SFLogger logger = SFLoggerFactory.getLogger(PlatformDetectionUtil.class);
18+
19+
public static String performPlatformDetectionRequest(HttpRequestBase httpRequest, int timeoutMs)
20+
throws SnowflakeSQLException, IOException {
21+
return HttpUtil.executeGeneralRequestOmitSnowflakeHeaders(
22+
httpRequest,
23+
1,
24+
timeoutMs / 1000,
25+
timeoutMs,
26+
0,
27+
new HttpClientSettingsKey(OCSPMode.DISABLE_OCSP_CHECKS),
28+
null);
29+
}
30+
31+
public static boolean hasValidAwsIdentityForWif(AwsAttestationService attestationService) {
32+
if (!hasValidAwsCredentials(attestationService)) {
33+
logger.debug("No valid AWS credentials available for identity validation");
34+
return false;
35+
}
36+
return true;
37+
}
38+
39+
public static boolean hasValidAwsCredentials(AwsAttestationService attestationService) {
40+
try {
41+
AWSCredentials credentials = attestationService.getAWSCredentials();
42+
return hasValidAwsCredentials(credentials);
43+
} catch (Exception e) {
44+
logger.debug("Failed to get AWS credentials from default provider chain: {}", e.getMessage());
45+
return false;
46+
}
47+
}
48+
49+
public static boolean hasValidAwsCredentials(AWSCredentials awsCredentials) {
50+
try {
51+
if (awsCredentials == null) {
52+
logger.debug("No AWS credentials found");
53+
return false;
54+
}
55+
56+
// Check if we have access key and secret key
57+
String accessKey = awsCredentials.getAWSAccessKeyId();
58+
String secretKey = awsCredentials.getAWSSecretKey();
59+
60+
if (accessKey == null
61+
|| accessKey.trim().isEmpty()
62+
|| secretKey == null
63+
|| secretKey.trim().isEmpty()) {
64+
logger.debug("AWS credentials are incomplete");
65+
return false;
66+
}
67+
68+
logger.debug("Valid AWS credentials found");
69+
return true;
70+
71+
} catch (Exception e) {
72+
logger.debug("AWS credential check failed: {}", e.getMessage());
73+
return false;
74+
}
75+
}
76+
}

src/main/java/net/snowflake/client/core/auth/wif/WorkloadIdentityUtil.java

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,33 @@
88
import net.snowflake.client.core.HttpUtil;
99
import net.snowflake.client.core.SFException;
1010
import net.snowflake.client.core.SFLoginInput;
11+
import net.snowflake.client.core.SnowflakeJdbcInternalApi;
1112
import net.snowflake.client.jdbc.ErrorCode;
1213
import net.snowflake.client.jdbc.SnowflakeSQLException;
1314
import net.snowflake.client.log.SFLogger;
1415
import net.snowflake.client.log.SFLoggerFactory;
1516
import org.apache.http.client.methods.HttpRequestBase;
1617

17-
class WorkloadIdentityUtil {
18+
/**
19+
* Utility class for Workload Identity Federation (WIF) specific operations. This class contains
20+
* functions that are used exclusively within the WIF package.
21+
*/
22+
@SnowflakeJdbcInternalApi
23+
public class WorkloadIdentityUtil {
1824

1925
private static final SFLogger logger = SFLoggerFactory.getLogger(WorkloadIdentityUtil.class);
2026

21-
static final String SNOWFLAKE_AUDIENCE_HEADER_NAME = "X-Snowflake-Audience";
22-
static final String SNOWFLAKE_AUDIENCE = "snowflakecomputing.com";
23-
2427
// Address commonly used by AWS, Azure & GCP to host instance metadata service
25-
static final String DEFAULT_METADATA_SERVICE_BASE_URL = "http://169.254.169.254";
28+
public static final String DEFAULT_METADATA_SERVICE_BASE_URL = "http://169.254.169.254";
29+
30+
public static final String SNOWFLAKE_AUDIENCE_HEADER_NAME = "X-Snowflake-Audience";
31+
public static final String SNOWFLAKE_AUDIENCE = "snowflakecomputing.com";
2632

27-
static String performIdentityRequest(HttpRequestBase tokenRequest, SFLoginInput loginInput)
33+
/**
34+
* Performs an HTTP request for WIF identity token retrieval. This method is used by WIF
35+
* authentication flows to communicate with cloud metadata services.
36+
*/
37+
public static String performIdentityRequest(HttpRequestBase tokenRequest, SFLoginInput loginInput)
2838
throws SnowflakeSQLException, IOException {
2939
return HttpUtil.executeGeneralRequestOmitSnowflakeHeaders(
3040
tokenRequest,
@@ -36,7 +46,12 @@ static String performIdentityRequest(HttpRequestBase tokenRequest, SFLoginInput
3646
null);
3747
}
3848

39-
static SubjectAndIssuer extractClaimsWithoutVerifyingSignature(String token) throws SFException {
49+
/**
50+
* Extracts claims (subject and issuer) from a JWT token without verifying the signature. This is
51+
* used in WIF flows where signature verification is handled elsewhere.
52+
*/
53+
public static SubjectAndIssuer extractClaimsWithoutVerifyingSignature(String token)
54+
throws SFException {
4055
Map<String, Object> claims = extractClaimsMap(token);
4156
if (claims == null) {
4257
throw new SFException(
@@ -65,11 +80,15 @@ private static Map<String, Object> extractClaimsMap(String token) {
6580
}
6681
}
6782

68-
static class SubjectAndIssuer {
83+
/**
84+
* Container class for JWT subject and issuer claims. Used in WIF authentication flows to pass
85+
* extracted token claims.
86+
*/
87+
public static class SubjectAndIssuer {
6988
private final String subject;
7089
private final String issuer;
7190

72-
SubjectAndIssuer(String subject, String issuer) {
91+
public SubjectAndIssuer(String subject, String issuer) {
7392
this.issuer = issuer;
7493
this.subject = subject;
7594
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package net.snowflake.client.util;
2+
3+
import net.snowflake.client.core.SnowflakeJdbcInternalApi;
4+
5+
/**
6+
* Interface for providing environment variables to enable thread-safe testing. This abstraction
7+
* allows dependency injection of environment variable access, making code testable with instance
8+
* mocks (as opposed to static SnowflakeUtil) that work across threads.
9+
*/
10+
@SnowflakeJdbcInternalApi
11+
public interface EnvironmentProvider {
12+
13+
String getEnv(String name);
14+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package net.snowflake.client.util;
2+
3+
import net.snowflake.client.core.SnowflakeJdbcInternalApi;
4+
5+
/** Enum representing all detectable cloud platforms and identity providers. */
6+
@SnowflakeJdbcInternalApi
7+
public enum Platform {
8+
IS_AWS_LAMBDA("is_aws_lambda"),
9+
IS_AZURE_FUNCTION("is_azure_function"),
10+
IS_GCE_CLOUD_RUN_SERVICE("is_gce_cloud_run_service"),
11+
IS_GCE_CLOUD_RUN_JOB("is_gce_cloud_run_job"),
12+
IS_GITHUB_ACTION("is_github_action"),
13+
IS_EC2_INSTANCE("is_ec2_instance"),
14+
HAS_AWS_IDENTITY_ARN_UNKNOWN("has_aws_identity_arn_unknown"),
15+
IS_AZURE_VM("is_azure_vm"),
16+
HAS_AZURE_MANAGED_IDENTITY("has_azure_managed_identity"),
17+
IS_GCE_VM("is_gce_vm"),
18+
HAS_GCP_IDENTITY("has_gcp_identity");
19+
20+
private final String value;
21+
22+
Platform(String value) {
23+
this.value = value;
24+
}
25+
26+
public String getValue() {
27+
return value;
28+
}
29+
30+
@Override
31+
public String toString() {
32+
return value;
33+
}
34+
}

0 commit comments

Comments
 (0)