-
Notifications
You must be signed in to change notification settings - Fork 536
Pmansour/snow 1927956 wif #2195
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
Changes from all commits
8f2adc0
605c636
a0eb05c
d6d8bdd
2d6efc1
cd49eaf
06d74bd
36b7e20
0c17d31
e245c9c
4c6159f
a3f9f60
2ee89bb
2601335
ca1f0a6
22d3cd7
8b2d45f
e850268
aac04ed
bae5987
e6c6a22
5ed7ad7
fc1577d
5ccc5c6
85b57b4
d8739d1
fd38e2f
1a0e3af
c4570e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| # | ||
| # Copyright (c) 2012-2025 Snowflake Computing Inc. All rights reserved. | ||
| # | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from enum import Enum, unique | ||
| import json | ||
| import typing | ||
|
|
||
| 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 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): | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this class meant to be used in multithreaded environment? It is not thread safe.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you please elaborate on this part? I believe it's using the same pattern as all the other authenticator plugins. I am not sure if authenticator plugins generally need to be thread-safe, or if the other ones are (or aren't), or if this one is different. |
||
| """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]: | ||
| self.reset_secrets() | ||
| self.prepare() | ||
| return {"success": True} | ||
|
|
||
| @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=(',', ':')) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ | |
| AuthByPlugin, | ||
| AuthByUsrPwdMfa, | ||
| AuthByWebBrowser, | ||
| AuthByWorkloadIdentity, | ||
sfc-gh-pmansour marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| AuthNoAuth, | ||
| ) | ||
| from .auth.idtoken import AuthByIdToken | ||
|
|
@@ -56,6 +57,7 @@ | |
| _CONNECTIVITY_ERR_MSG, | ||
| _DOMAIN_NAME_MAP, | ||
| ENV_VAR_PARTNER, | ||
| ENV_VAR_EXPERIMENTAL_AUTHENTICATION, | ||
| PARAMETER_AUTOCOMMIT, | ||
| PARAMETER_CLIENT_PREFETCH_THREADS, | ||
| PARAMETER_CLIENT_REQUEST_MFA_TOKEN, | ||
|
|
@@ -92,6 +94,7 @@ | |
| ER_NO_PASSWORD, | ||
| ER_NO_USER, | ||
| ER_NOT_IMPLICITY_SNOWFLAKE_DATATYPE, | ||
| ER_INVALID_WIF_SETTINGS, | ||
| ) | ||
| from .errors import DatabaseError, Error, OperationalError, ProgrammingError | ||
| from .log_configuration import EasyLoggingConfigPython | ||
|
|
@@ -104,6 +107,7 @@ | |
| PROGRAMMATIC_ACCESS_TOKEN, | ||
| REQUEST_ID, | ||
| USR_PWD_MFA_AUTHENTICATOR, | ||
| WORKLOAD_IDENTITY_AUTHENTICATOR, | ||
| ReauthenticationRequest, | ||
| SnowflakeRestful, | ||
| ) | ||
|
|
@@ -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 | ||
|
|
@@ -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)), | ||
|
|
@@ -1113,6 +1120,23 @@ def __open_connection(self): | |
| ) | ||
| elif self._authenticator == PROGRAMMATIC_ACCESS_TOKEN: | ||
| self.auth_class = AuthByPAT(self._token) | ||
| elif self._authenticator == WORKLOAD_IDENTITY_AUTHENTICATOR: | ||
| if not ENV_VAR_EXPERIMENTAL_AUTHENTICATION 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, | ||
| }, | ||
| ) | ||
| 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( | ||
|
|
@@ -1241,6 +1265,7 @@ def __config(self, **kwargs): | |
| KEY_PAIR_AUTHENTICATOR, | ||
| OAUTH_AUTHENTICATOR, | ||
| USR_PWD_MFA_AUTHENTICATOR, | ||
| WORKLOAD_IDENTITY_AUTHENTICATOR, | ||
| ]: | ||
| self._authenticator = auth_tmp | ||
|
|
||
|
|
@@ -1251,14 +1276,14 @@ 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} | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, I believe external browser and PAT also don't need user... @sfc-gh-mmishchenko wdyt?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth checking, but that's not probably a subject of this review. |
||
|
|
||
| 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, | ||
|
|
@@ -1269,6 +1294,19 @@ 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 | ||
|
|
@@ -1277,6 +1315,7 @@ def __config(self, **kwargs): | |
| OAUTH_AUTHENTICATOR, | ||
| KEY_PAIR_AUTHENTICATOR, | ||
| PROGRAMMATIC_ACCESS_TOKEN, | ||
| WORKLOAD_IDENTITY_AUTHENTICATOR, | ||
| ) | ||
| and not self._password | ||
| ): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have risk assesment for these new libraries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we do not -- what's the process for getting that?
FWIW they're the standard AWS client libraries and are maintained by AWS, so I suspect it'll be fine, but if you can point me at a process I'd be happy to follow that.