Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
97b021d
Add AWS dependencies
sfc-gh-pmansour Feb 28, 2025
8bde498
add basic attestation loading
sfc-gh-pmansour Mar 1, 2025
d215fa7
improve the contract for fetching WIF attestations
sfc-gh-pmansour Mar 1, 2025
f01e0b2
Add explicit attestation provider type, support for OIDC tokens
sfc-gh-pmansour Mar 4, 2025
2fd8825
add associated error codes
sfc-gh-pmansour Mar 4, 2025
05a6d91
fix small bugs, clean-up data structures, add auto-detect provider
sfc-gh-pmansour Mar 4, 2025
b491f39
fix docstrings
sfc-gh-pmansour Mar 4, 2025
4ed9819
add an entry point
sfc-gh-pmansour Mar 4, 2025
a4453b0
add WLID authenticator plugin
sfc-gh-pmansour Mar 4, 2025
536da37
Add glue for WLID authenticator
sfc-gh-pmansour Mar 4, 2025
c2292f2
Remove explicit handling of token_file_path since this is done in con…
sfc-gh-pmansour Mar 4, 2025
98fe189
Add unit tests for connection plumbing
sfc-gh-pmansour Mar 4, 2025
206d383
fix bug with default value of workload_identity_provider
sfc-gh-pmansour Mar 4, 2025
f5052f6
small bug fix in HTTP request
sfc-gh-pmansour Mar 4, 2025
e5690fb
update logic for assertion_content
sfc-gh-pmansour Mar 4, 2025
986a5b2
fix whitespace
sfc-gh-pmansour Mar 4, 2025
4136a5a
update error message
sfc-gh-pmansour Mar 4, 2025
88aa743
wrap metadata server requests in a try..except block
sfc-gh-pmansour Mar 5, 2025
7b0b895
include issuer in Azure identifier string
sfc-gh-pmansour Mar 5, 2025
c08a77e
add unit tests
sfc-gh-pmansour Mar 5, 2025
da6db75
change order of constants
sfc-gh-pmansour Mar 5, 2025
873bdcc
Slight refactor of metadata calls, added debug logs for missing crede…
sfc-gh-pmansour Mar 5, 2025
3a746df
refactor token parsing
sfc-gh-pmansour Mar 5, 2025
deaa66c
gate the feature on the preview env variable
sfc-gh-pmansour Mar 5, 2025
d416752
update autodetect logic for OIDC
sfc-gh-pmansour Mar 5, 2025
007380c
allow explicit setting of entra resource, don't ask for account
sfc-gh-pmansour Mar 5, 2025
75f850f
add tests for Entra resource plumbing
sfc-gh-pmansour Mar 5, 2025
814cb28
add support for AWS region and arn loading
sfc-gh-pmansour Mar 6, 2025
724940d
add support for azure functions
sfc-gh-pmansour Mar 6, 2025
9238e81
Extract CSP mocking logic to a separate file with context managers an…
sfc-gh-pmansour Mar 7, 2025
f24dcf6
Replace placeholder Azure resource with fake one that will break
sfc-gh-pmansour Mar 7, 2025
e3275dd
remove stale todos
sfc-gh-pmansour Mar 7, 2025
79c813c
use extracted helper function
sfc-gh-pmansour Mar 7, 2025
3d25921
use dict that was extracted earlier in test helper
sfc-gh-pmansour Mar 7, 2025
4087ca6
Update comments for test helpers
sfc-gh-pmansour Mar 7, 2025
02886e5
update test verification for AWS tokens
sfc-gh-pmansour Mar 7, 2025
80ac41d
minor test changes
sfc-gh-pmansour Mar 7, 2025
8cc8f38
updated JWT parsing error handling
sfc-gh-pmansour Mar 7, 2025
b2a3ca9
Run formatter
sfc-gh-pmansour Mar 7, 2025
9bfdc2f
Move new fixtures to conftest.py
sfc-gh-pmansour Mar 7, 2025
ae7e252
Fix flake8 issues
sfc-gh-pmansour Mar 7, 2025
86fdda0
fix old driver tests by wrapping imports in a try..except block
sfc-gh-pmansour Mar 12, 2025
82e5810
slight changes for the PR
sfc-gh-pmansour Mar 12, 2025
5e6f137
update fix for old driver tests
sfc-gh-pmansour Mar 12, 2025
525c746
fix for old driver imports
sfc-gh-pmansour Mar 12, 2025
e7a8bc8
Move newer fixtures to unit-test specific conftest file
sfc-gh-pmansour Mar 12, 2025
54d87b1
Fix how we're defining default WLID-specific params
sfc-gh-pmansour Mar 12, 2025
467d59d
don't run old driver tests against unit tests or pandas tests
sfc-gh-pmansour Mar 12, 2025
5400f25
Make AuthByWorkloadIdentity only accept keyword args
sfc-gh-pmansour Mar 14, 2025
3794f9f
Make reauthenticate a no-op
sfc-gh-pmansour Mar 14, 2025
d8638d8
Add explanation for tox.ini olddriver change
sfc-gh-pmansour Mar 14, 2025
c95c29b
Update enum parsing from TOML, add test for TOML connection config
sfc-gh-pmansour Mar 14, 2025
b6bc1ee
Small fix for Windows paths
sfc-gh-pmansour Mar 17, 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
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ python_requires = >=3.8
packages = find_namespace:
install_requires =
asn1crypto>0.24.0,<2.0.0
boto3>=1.0
botocore>=1.0
cffi>=1.9,<2.0.0
cryptography>=3.1.0
pyOpenSSL>=22.0.0,<26.0.0
Expand Down
3 changes: 3 additions & 0 deletions src/snowflake/connector/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .pat import AuthByPAT
from .usrpwdmfa import AuthByUsrPwdMfa
from .webbrowser import AuthByWebBrowser
from .workload_identity import AuthByWorkloadIdentity

FIRST_PARTY_AUTHENTICATORS = frozenset(
(
Expand All @@ -26,6 +27,7 @@
AuthByWebBrowser,
AuthByIdToken,
AuthByPAT,
AuthByWorkloadIdentity,
AuthNoAuth,
)
)
Expand All @@ -39,6 +41,7 @@
"AuthByOkta",
"AuthByUsrPwdMfa",
"AuthByWebBrowser",
"AuthByWorkloadIdentity",
"AuthNoAuth",
"Auth",
"AuthType",
Expand Down
1 change: 1 addition & 0 deletions src/snowflake/connector/auth/by_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class AuthType(Enum):
OKTA = "OKTA"
PAT = "PROGRAMMATIC_ACCESS_TOKEN'"
NO_AUTH = "NO_AUTH"
WORKLOAD_IDENTITY = "WORKLOAD_IDENTITY"


class AuthByPlugin(ABC):
Expand Down
98 changes: 98 additions & 0 deletions src/snowflake/connector/auth/workload_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#
# Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
#

from __future__ import annotations

import json
import typing
from enum import Enum, unique

from ..network import WORKLOAD_IDENTITY_AUTHENTICATOR
from ..wif_util import (
AttestationProvider,
WorkloadIdentityAttestation,
create_attestation,
)
from .by_plugin import AuthByPlugin, AuthType


@unique
class ApiFederatedAuthenticationType(Enum):
"""An API-specific enum of the WIF authentication type."""

AWS = "AWS"
AZURE = "AZURE"
GCP = "GCP"
OIDC = "OIDC"

@staticmethod
def from_attestation(
attestation: WorkloadIdentityAttestation,
) -> ApiFederatedAuthenticationType:
"""Maps the internal / driver-specific attestation providers to API authenticator types.

The AttestationProvider is related to how the driver fetches the credential, while the API authenticator
type is related to how the credential is verified. In most current cases these may be the same, though
in the future we could have, for example, multiple AttestationProviders that all fetch an OIDC ID token.
"""
if attestation.provider == AttestationProvider.AWS:
return ApiFederatedAuthenticationType.AWS
if attestation.provider == AttestationProvider.AZURE:
return ApiFederatedAuthenticationType.AZURE
if attestation.provider == AttestationProvider.GCP:
return ApiFederatedAuthenticationType.GCP
if attestation.provider == AttestationProvider.OIDC:
return ApiFederatedAuthenticationType.OIDC
return ValueError(f"Unknown attestation provider '{attestation.provider}'")


class AuthByWorkloadIdentity(AuthByPlugin):
"""Plugin to authenticate via workload identity."""

def __init__(
self,
*,
provider: AttestationProvider | None = None,
token: str | None = None,
entra_resource: str | None = None,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.provider = provider
self.token = token
self.entra_resource = entra_resource

self.attestation: WorkloadIdentityAttestation | None = None

def type_(self) -> AuthType:
return AuthType.WORKLOAD_IDENTITY

def reset_secrets(self) -> None:
self.attestation = None

def update_body(self, body: dict[typing.Any, typing.Any]) -> None:
body["data"]["AUTHENTICATOR"] = WORKLOAD_IDENTITY_AUTHENTICATOR
body["data"]["PROVIDER"] = ApiFederatedAuthenticationType.from_attestation(
self.attestation
).value
body["data"]["TOKEN"] = self.attestation.credential

def prepare(self, **kwargs: typing.Any) -> None:
"""Fetch the token."""
self.attestation = create_attestation(
self.provider, self.entra_resource, self.token
)

def reauthenticate(self, **kwargs: typing.Any) -> dict[str, bool]:
"""This is only relevant for AuthByIdToken, which uses a web-browser based flow. All other auth plugins just call authenticate() again."""
return {"success": False}

@property
def assertion_content(self) -> str:
"""Returns the CSP provider name and an identifier. Used for logging purposes."""
if not self.attestation:
return ""
properties = self.attestation.user_identifier_components
properties["_provider"] = self.attestation.provider.value
return json.dumps(properties, sort_keys=True, separators=(",", ":"))
63 changes: 59 additions & 4 deletions src/snowflake/connector/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
AuthByPlugin,
AuthByUsrPwdMfa,
AuthByWebBrowser,
AuthByWorkloadIdentity,
AuthNoAuth,
)
from .auth.idtoken import AuthByIdToken
Expand All @@ -55,6 +56,7 @@
from .constants import (
_CONNECTIVITY_ERR_MSG,
_DOMAIN_NAME_MAP,
ENV_VAR_EXPERIMENTAL_AUTHENTICATION,
ENV_VAR_PARTNER,
PARAMETER_AUTOCOMMIT,
PARAMETER_CLIENT_PREFETCH_THREADS,
Expand Down Expand Up @@ -87,6 +89,7 @@
ER_FAILED_TO_CONNECT_TO_DB,
ER_INVALID_BACKOFF_POLICY,
ER_INVALID_VALUE,
ER_INVALID_WIF_SETTINGS,
ER_NO_ACCOUNT_NAME,
ER_NO_NUMPY,
ER_NO_PASSWORD,
Expand All @@ -104,6 +107,7 @@
PROGRAMMATIC_ACCESS_TOKEN,
REQUEST_ID,
USR_PWD_MFA_AUTHENTICATOR,
WORKLOAD_IDENTITY_AUTHENTICATOR,
ReauthenticationRequest,
SnowflakeRestful,
)
Expand All @@ -112,6 +116,7 @@
from .time_util import HeartBeatTimer, get_time_millis
from .url_util import extract_top_level_domain_from_hostname
from .util_text import construct_hostname, parse_account, split_statements
from .wif_util import AttestationProvider

DEFAULT_CLIENT_PREFETCH_THREADS = 4
MAX_CLIENT_PREFETCH_THREADS = 10
Expand Down Expand Up @@ -188,12 +193,14 @@ def _get_private_bytes_from_file(
"private_key": (None, (type(None), bytes, str, RSAPrivateKey)),
"private_key_file": (None, (type(None), str)),
"private_key_file_pwd": (None, (type(None), str, bytes)),
"token": (None, (type(None), str)), # OAuth/JWT/PAT Token
"token": (None, (type(None), str)), # OAuth/JWT/PAT/OIDC Token
"token_file_path": (
None,
(type(None), str, bytes),
), # OAuth/JWT/PAT Token file path
), # OAuth/JWT/PAT/OIDC Token file path
"authenticator": (DEFAULT_AUTHENTICATOR, (type(None), str)),
"workload_identity_provider": (None, (type(None), AttestationProvider)),
"workload_identity_entra_resource": (None, (type(None), str)),
"mfa_callback": (None, (type(None), Callable)),
"password_callback": (None, (type(None), Callable)),
"auth_class": (None, (type(None), AuthByPlugin)),
Expand Down Expand Up @@ -1140,6 +1147,29 @@ def __open_connection(self):
if not self._token and self._password:
self._token = self._password
self.auth_class = AuthByPAT(self._token)
elif self._authenticator == WORKLOAD_IDENTITY_AUTHENTICATOR:
if ENV_VAR_EXPERIMENTAL_AUTHENTICATION not in os.environ:
Error.errorhandler_wrapper(
self,
None,
ProgrammingError,
{
"msg": f"Please set the '{ENV_VAR_EXPERIMENTAL_AUTHENTICATION}' environment variable to use the '{WORKLOAD_IDENTITY_AUTHENTICATOR}' authenticator.",
"errno": ER_INVALID_WIF_SETTINGS,
},
)
# Standardize the provider enum.
if self._workload_identity_provider and isinstance(
self._workload_identity_provider, str
):
self._workload_identity_provider = AttestationProvider.from_string(
self._workload_identity_provider
)
self.auth_class = AuthByWorkloadIdentity(
provider=self._workload_identity_provider,
token=self._token,
entra_resource=self._workload_identity_entra_resource,
)
else:
# okta URL, e.g., https://<account>.okta.com/
self.auth_class = AuthByOkta(
Expand Down Expand Up @@ -1268,6 +1298,7 @@ def __config(self, **kwargs):
KEY_PAIR_AUTHENTICATOR,
OAUTH_AUTHENTICATOR,
USR_PWD_MFA_AUTHENTICATOR,
WORKLOAD_IDENTITY_AUTHENTICATOR,
]:
self._authenticator = auth_tmp

Expand All @@ -1278,14 +1309,18 @@ def __config(self, **kwargs):
self._token = f.read()

# Set of authenticators allowing empty user.
empty_user_allowed_authenticators = {OAUTH_AUTHENTICATOR, NO_AUTH_AUTHENTICATOR}
empty_user_allowed_authenticators = {
OAUTH_AUTHENTICATOR,
NO_AUTH_AUTHENTICATOR,
WORKLOAD_IDENTITY_AUTHENTICATOR,
}

if not (self._master_token and self._session_token):
if (
not self.user
and self._authenticator not in empty_user_allowed_authenticators
):
# OAuth and NoAuth Authentications does not require a username
# Some authenticators do not require a username
Error.errorhandler_wrapper(
self,
None,
Expand All @@ -1296,6 +1331,25 @@ def __config(self, **kwargs):
if self._private_key or self._private_key_file:
self._authenticator = KEY_PAIR_AUTHENTICATOR

workload_identity_dependent_options = [
"workload_identity_provider",
"workload_identity_entra_resource",
]
for dependent_option in workload_identity_dependent_options:
if (
self.__getattribute__(f"_{dependent_option}") is not None
and self._authenticator != WORKLOAD_IDENTITY_AUTHENTICATOR
):
Error.errorhandler_wrapper(
self,
None,
ProgrammingError,
{
"msg": f"{dependent_option} was set but authenticator was not set to {WORKLOAD_IDENTITY_AUTHENTICATOR}",
"errno": ER_INVALID_WIF_SETTINGS,
},
)

if (
self.auth_class is None
and self._authenticator
Expand All @@ -1304,6 +1358,7 @@ def __config(self, **kwargs):
OAUTH_AUTHENTICATOR,
KEY_PAIR_AUTHENTICATOR,
PROGRAMMATIC_ACCESS_TOKEN,
WORKLOAD_IDENTITY_AUTHENTICATOR,
)
and not self._password
):
Expand Down
1 change: 1 addition & 0 deletions src/snowflake/connector/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ class IterUnit(Enum):
# TODO: all env variables definitions should be here
ENV_VAR_PARTNER = "SF_PARTNER"
ENV_VAR_TEST_MODE = "SNOWFLAKE_TEST_MODE"
ENV_VAR_EXPERIMENTAL_AUTHENTICATION = "SF_ENABLE_EXPERIMENTAL_AUTHENTICATION" # Needed to enable new strong auth features during the private preview.


_DOMAIN_NAME_MAP = {_DEFAULT_HOSTNAME_TLD: "GLOBAL", _CHINA_HOSTNAME_TLD: "CHINA"}
Expand Down
2 changes: 2 additions & 0 deletions src/snowflake/connector/errorcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
ER_JWT_RETRY_EXPIRED = 251010
ER_CONNECTION_TIMEOUT = 251011
ER_RETRYABLE_CODE = 251012
ER_INVALID_WIF_SETTINGS = 251013
ER_WIF_CREDENTIALS_NOT_FOUND = 251014

# cursor
ER_FAILED_TO_REWRITE_MULTI_ROW_INSERT = 252001
Expand Down
1 change: 1 addition & 0 deletions src/snowflake/connector/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
USR_PWD_MFA_AUTHENTICATOR = "USERNAME_PASSWORD_MFA"
PROGRAMMATIC_ACCESS_TOKEN = "PROGRAMMATIC_ACCESS_TOKEN"
NO_AUTH_AUTHENTICATOR = "NO_AUTH"
WORKLOAD_IDENTITY_AUTHENTICATOR = "WORKLOAD_IDENTITY"


def is_retryable_http_code(code: int) -> bool:
Expand Down
Loading
Loading