Skip to content

Snow-2183023: Workload Identity Federation with explicit http requests #2439

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

Open
wants to merge 58 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
11d88e9
SNOW-2183023: Session manager refactored
sfc-gh-fpawlowski Jul 7, 2025
142fb68
Merge branch 'main' into SNOW-2183023-Python-WIF-in-Driver-SDK-boto3-…
sfc-gh-fpawlowski Jul 7, 2025
c375164
SNOW-2183023: Added adapter factory
sfc-gh-fpawlowski Jul 7, 2025
e820b4d
SNOW-2183023: Adapters fixed in tests
sfc-gh-fpawlowski Jul 7, 2025
965786a
SNOW-2183023: Removed boto
sfc-gh-fpawlowski Jul 7, 2025
f8b0944
SNOW-2183023: Removed boto
sfc-gh-fpawlowski Jul 7, 2025
f872fd2
SNOW-2183023: SEssions map access fixed
sfc-gh-fpawlowski Jul 7, 2025
95ace1f
SNOW-2183023: use pooling
sfc-gh-fpawlowski Jul 7, 2025
53087d4
SNOW-2183023: refactored
sfc-gh-fpawlowski Jul 7, 2025
23e338b
SNOW-2183023: fixed fake env
sfc-gh-fpawlowski Jul 7, 2025
3faa942
Revert "SNOW-2183023: refactored"
sfc-gh-fpawlowski Jul 7, 2025
43f7868
Reapply "SNOW-2183023: refactored"
sfc-gh-fpawlowski Jul 7, 2025
2241969
SNOW-2183023: fixed csp_helper
sfc-gh-fpawlowski Jul 7, 2025
4f0785c
Revert "Reapply "SNOW-2183023: refactored""
sfc-gh-fpawlowski Jul 7, 2025
e603c1c
Reapply "Reapply "SNOW-2183023: refactored""
sfc-gh-fpawlowski Jul 7, 2025
fde7556
SNOW-2183023: fixed csp_helper and added boto as dev-dep
sfc-gh-fpawlowski Jul 8, 2025
023f0ae
SNOW-2183023: added arn fetch
sfc-gh-fpawlowski Jul 8, 2025
3fef4e7
Revert "Reapply "Reapply "SNOW-2183023: refactored"""
sfc-gh-fpawlowski Jul 8, 2025
2e23fe3
Reapply "Reapply "Reapply "SNOW-2183023: refactored"""
sfc-gh-fpawlowski Jul 8, 2025
59a3373
SNOW-2183023: fixed missing prepare args
sfc-gh-fpawlowski Jul 8, 2025
093b5b5
SNOW-2183023: fixed missing prepare args
sfc-gh-fpawlowski Jul 8, 2025
219e8c6
SNOW-2183023: fixed patching
sfc-gh-fpawlowski Jul 8, 2025
b486239
SNOW-2183023: fixed tests
sfc-gh-fpawlowski Jul 8, 2025
af2f4a3
SNOW-2183023: fixed tests
sfc-gh-fpawlowski Jul 8, 2025
88ccb83
SNOW-2183023: fixed tests
sfc-gh-fpawlowski Jul 8, 2025
9fbda91
SNOW-2183023: removed http_client for now
sfc-gh-fpawlowski Jul 8, 2025
5e921d8
SNOW-2183023: fixed test proxies
sfc-gh-fpawlowski Jul 8, 2025
a3faefd
SNOW-2183023: fixed user id
sfc-gh-fpawlowski Jul 8, 2025
a2c0295
SNOW-2183023: fixed mock
sfc-gh-fpawlowski Jul 8, 2025
e176195
wif credential working
sfc-gh-fpawlowski Jul 9, 2025
1efb814
get region working
sfc-gh-fpawlowski Jul 9, 2025
ec76ade
signing not yet working
sfc-gh-fpawlowski Jul 9, 2025
12cf2cd
that worked
sfc-gh-fpawlowski Jul 9, 2025
0151c7d
boto3 removed
sfc-gh-fpawlowski Jul 10, 2025
39b3267
botocore removed. cleanup
sfc-gh-fpawlowski Jul 10, 2025
110a63b
botocore removed. cleanup
sfc-gh-fpawlowski Jul 10, 2025
bd58179
cleanup to own Credential class
sfc-gh-fpawlowski Jul 11, 2025
564ac06
refactored files
sfc-gh-fpawlowski Jul 11, 2025
8df9bdf
arn from env vars or only region
sfc-gh-fpawlowski Jul 12, 2025
04a11d8
tests working
sfc-gh-fpawlowski Jul 12, 2025
f018af5
botocore compatibility tests
sfc-gh-fpawlowski Jul 12, 2025
b5e3554
finished boto comp
sfc-gh-fpawlowski Jul 13, 2025
6ab7a50
SNOW-2183023: removed session manager
sfc-gh-fpawlowski Jul 13, 2025
e85216a
Refactored without sessionManager - split into 2 PRs
sfc-gh-fpawlowski Jul 13, 2025
bbf5052
SNOW-2183023: slight improved tests
sfc-gh-fpawlowski Jul 13, 2025
35e23cf
SNOW-2183023: TEMP commit metadata service initial intro
sfc-gh-fpawlowski Jul 13, 2025
0b8c7a6
SNOW-2183023: All tests passing with many aws envs
sfc-gh-fpawlowski Jul 13, 2025
f1fd5fa
SNOW-2183023: all tests pass and only http is mocked
sfc-gh-fpawlowski Jul 13, 2025
855ff4f
Merge branch 'main' into SNOW-2183023-Python-WIF-in-Driver-SDK-boto3-…
sfc-gh-fpawlowski Jul 13, 2025
2bc2825
SNOW-2183023: base fix of constants approach
sfc-gh-fpawlowski Jul 13, 2025
ea742d6
SNOW-2183023: base fix of constants approach
sfc-gh-fpawlowski Jul 13, 2025
0bee4cd
SNOW-2183023: this breaks code a lot - wrong order of envs cleanup
sfc-gh-fpawlowski Jul 13, 2025
e3a00ab
SNOW-2183023: breaks code less - wrong order of envs cleanup
sfc-gh-fpawlowski Jul 13, 2025
fbbc223
SNOW-2183023: fixed http traffix
sfc-gh-fpawlowski Jul 13, 2025
fab3af9
SNOW-2183023: fixed http traffix
sfc-gh-fpawlowski Jul 13, 2025
f9b8918
SNOW-2183023: comments cleanup
sfc-gh-fpawlowski Jul 13, 2025
935efd1
SNOW-2183023: http session unmanaged requests
sfc-gh-fpawlowski Jul 13, 2025
f49cdcb
Merge branch 'dev/trunk' into SNOW-2183023-Python-WIF-in-Driver-SDK-b…
sfc-gh-fpawlowski Jul 28, 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
3 changes: 2 additions & 1 deletion DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ https://docs.snowflake.com/
Source code is also available at: https://github.com/snowflakedb/snowflake-connector-python

# Release Notes
- v3.16.1(TBD)
- v3.17(TBD)
- Removed boto and botocore dependencies.
- Added in-band OCSP exception telemetry.
- Added `APPLICATION_PATH` within `CLIENT_ENVIRONMENT` to distinguish between multiple scripts using the PythonConnector in the same environment.
- Disabled token caching for OAuth Client Credentials authentication
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ python_requires = >=3.9
packages = find_namespace:
install_requires =
asn1crypto>0.24.0,<2.0.0
boto3>=1.24
botocore>=1.24
cffi>=1.9,<2.0.0
cryptography>=3.1.0
pyOpenSSL>=22.0.0,<26.0.0
Expand Down Expand Up @@ -92,6 +90,8 @@ development =
pytest-timeout
pytest-xdist
pytzdata
botocore
boto3
pandas =
pandas>=2.1.2,<3.0.0
pyarrow<19.0.0
Expand Down
136 changes: 136 additions & 0 deletions src/snowflake/connector/_aws_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""
Lightweight AWS credential resolution without boto3.

Resolves credentials in the order: environment → ECS/EKS task metadata → EC2 IMDSv2.
Returns a minimal `SfAWSCredentials` object that can be passed to SigV4 signing
helpers unchanged.
"""

from __future__ import annotations

import logging
import os
from dataclasses import dataclass
from functools import partial
from typing import Callable

from .vendored import requests

logger = logging.getLogger(__name__)

_ECS_CRED_BASE_URL = "http://169.254.170.2"
_IMDS_BASE_URL = "http://169.254.169.254"
_IMDS_TOKEN_PATH = "/latest/api/token"
_IMDS_ROLE_PATH = "/latest/meta-data/iam/security-credentials/"
_IMDS_AZ_PATH = "/latest/meta-data/placement/availability-zone"


@dataclass
class SfAWSCredentials:
"""Minimal stand-in for ``botocore.credentials.Credentials``."""

access_key: str
secret_key: str
token: str | None = None


def get_env_credentials() -> SfAWSCredentials | None:
key, secret = os.getenv("AWS_ACCESS_KEY_ID"), os.getenv("AWS_SECRET_ACCESS_KEY")
if key and secret:
return SfAWSCredentials(key, secret, os.getenv("AWS_SESSION_TOKEN"))
return None


def get_container_credentials(*, timeout: float) -> SfAWSCredentials | None:
"""Credentials from ECS/EKS task-metadata endpoint."""
rel_uri = os.getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
full_uri = os.getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")
if not rel_uri and not full_uri:
return None

url = full_uri or f"{_ECS_CRED_BASE_URL}{rel_uri}"
try:
response = requests.get(url, timeout=timeout)
if response.ok:
data = response.json()
return SfAWSCredentials(
data["AccessKeyId"], data["SecretAccessKey"], data.get("Token")
)
except (requests.Timeout, requests.ConnectionError, ValueError) as exc:
logger.debug("ECS credential fetch failed: %s", exc, exc_info=True)
return None


def _get_imds_v2_token(timeout: float) -> str | None:
try:
response = requests.request(
"PUT",
f"{_IMDS_BASE_URL}{_IMDS_TOKEN_PATH}",
headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
timeout=timeout,
)
return response.text if response.ok else None
except (requests.Timeout, requests.ConnectionError):
return None


def get_imds_credentials(*, timeout: float) -> SfAWSCredentials | None:
"""Instance-profile credentials from the EC2 metadata service."""
token = _get_imds_v2_token(timeout)
headers = {"X-aws-ec2-metadata-token": token} if token else {}

try:
role_resp = requests.get(
f"{_IMDS_BASE_URL}{_IMDS_ROLE_PATH}", headers=headers, timeout=timeout
)
if not role_resp.ok:
return None
role_name = role_resp.text.strip()

cred_resp = requests.get(
f"{_IMDS_BASE_URL}{_IMDS_ROLE_PATH}{role_name}",
headers=headers,
timeout=timeout,
)
if cred_resp.ok:
data = cred_resp.json()
return SfAWSCredentials(
data["AccessKeyId"], data["SecretAccessKey"], data.get("Token")
)
except (requests.Timeout, requests.ConnectionError, ValueError) as exc:
logger.debug("IMDS credential fetch failed: %s", exc, exc_info=True)
return None


def load_default_credentials(timeout: float = 2.0) -> SfAWSCredentials | None:
"""Resolve credentials using the default AWS chain (env → task → IMDS)."""
providers: tuple[Callable[[], SfAWSCredentials | None], ...] = (
get_env_credentials,
partial(get_container_credentials, timeout=timeout),
partial(get_imds_credentials, timeout=timeout),
)
for try_fetch_credentials in providers:
credentials = try_fetch_credentials()
if credentials:
return credentials
return None


def get_region(timeout: float = 1.0) -> str | None:
"""Return the current AWS region if it can be discovered."""
if region := os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION"):
return region

token = _get_imds_v2_token(timeout)
headers = {"X-aws-ec2-metadata-token": token} if token else {}
try:
response = requests.request(
"GET", f"{_IMDS_BASE_URL}{_IMDS_AZ_PATH}", headers=headers, timeout=timeout
)
if response.ok:
az = response.text.strip()
return az[:-1] if az and az[-1].isalpha() else None
except (requests.Timeout, requests.ConnectionError) as exc:
logger.debug("IMDS region lookup failed: %s", exc, exc_info=True)

return None
105 changes: 105 additions & 0 deletions src/snowflake/connector/_aws_sign_v4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import annotations

import datetime
import hashlib
import hmac
import urllib.parse as urlparse

_ALGORITHM: str = "AWS4-HMAC-SHA256"
_EMPTY_PAYLOAD_SHA256: str = hashlib.sha256(b"").hexdigest()
_SAFE_CHARS: str = "-_.~"


def _sign(key: bytes, msg: str) -> bytes:
"""Return an HMAC-SHA256 of *msg* keyed with *key*."""
return hmac.new(key, msg.encode(), hashlib.sha256).digest()


def _canonical_query_string(query: str) -> str:
"""Return the query string in canonical (sorted & URL-escaped) form."""
pairs = urlparse.parse_qsl(query, keep_blank_values=True)
pairs.sort()
return "&".join(
f"{urlparse.quote(k, _SAFE_CHARS)}={urlparse.quote(v, _SAFE_CHARS)}"
for k, v in pairs
)


def sign_get_caller_identity(
url: str,
region: str,
access_key: str,
secret_key: str,
session_token: str | None = None,
) -> dict[str, str]:
"""
Return the SigV4 headers needed for a presigned POST to AWS STS
`GetCallerIdentity`.

Parameters:

url
The full STS endpoint with query parameters
(e.g. ``https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15``)
region
The AWS region used for signing (``us-east-1``, ``us-gov-west-1`` …).
access_key
AWS access-key ID.
secret_key
AWS secret-access key.
session_token
(Optional) session token for temporary credentials.
"""
timestamp = datetime.datetime.utcnow()
amz_date = timestamp.strftime("%Y%m%dT%H%M%SZ")
short_date = timestamp.strftime("%Y%m%d")
service = "sts"

parsed = urlparse.urlparse(url)

headers: dict[str, str] = {
"host": parsed.netloc.lower(),
"x-amz-date": amz_date,
"x-snowflake-audience": "snowflakecomputing.com",
}
if session_token:
headers["x-amz-security-token"] = session_token

# Canonical request
signed_headers = ";".join(sorted(headers)) # e.g. host;x-amz-date;...
canonical_request = "\n".join(
(
"POST",
urlparse.quote(parsed.path or "/", safe="/"),
_canonical_query_string(parsed.query),
"".join(f"{k}:{headers[k]}\n" for k in sorted(headers)),
signed_headers,
_EMPTY_PAYLOAD_SHA256,
)
)
canonical_request_hash = hashlib.sha256(canonical_request.encode()).hexdigest()

# String to sign
credential_scope = f"{short_date}/{region}/{service}/aws4_request"
string_to_sign = "\n".join(
(_ALGORITHM, amz_date, credential_scope, canonical_request_hash)
)

# Signature
key_date = _sign(("AWS4" + secret_key).encode(), short_date)
key_region = _sign(key_date, region)
key_service = _sign(key_region, service)
key_signing = _sign(key_service, "aws4_request")
signature = hmac.new(
key_signing, string_to_sign.encode(), hashlib.sha256
).hexdigest()

# Final Authorization header
headers["authorization"] = (
f"{_ALGORITHM} "
f"Credential={access_key}/{credential_scope}, "
f"SignedHeaders={signed_headers}, "
f"Signature={signature}"
)

return headers
Loading
Loading