From df8cc566b06eb88c47f6b9ab80d9464a439976df Mon Sep 17 00:00:00 2001 From: Jakub Wilkowski Date: Wed, 29 Oct 2025 14:39:38 +0100 Subject: [PATCH 01/15] SNOW-2466332: Do not require user when using OAuth flow (#2606) (cherry picked from commit db2a10fa886bb197de6b1db7b50d796201fbec17) --- src/snowflake/connector/connection.py | 2 + test/unit/test_auth_oauth_auth_code.py | 43 ++++++++++++++++++++++ test/unit/test_auth_oauth_credentials.py | 47 ++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/snowflake/connector/connection.py b/src/snowflake/connector/connection.py index c4efe25f8c..bfb0553752 100644 --- a/src/snowflake/connector/connection.py +++ b/src/snowflake/connector/connection.py @@ -1545,6 +1545,8 @@ def __config(self, **kwargs): WORKLOAD_IDENTITY_AUTHENTICATOR, PROGRAMMATIC_ACCESS_TOKEN, PAT_WITH_EXTERNAL_SESSION, + OAUTH_AUTHORIZATION_CODE, + OAUTH_CLIENT_CREDENTIALS, } if not (self._master_token and self._session_token): diff --git a/test/unit/test_auth_oauth_auth_code.py b/test/unit/test_auth_oauth_auth_code.py index 76894791cc..4342521afa 100644 --- a/test/unit/test_auth_oauth_auth_code.py +++ b/test/unit/test_auth_oauth_auth_code.py @@ -285,3 +285,46 @@ def mock_request_tokens(self, **kwargs): assert isinstance(conn.auth_class, AuthByOauthCode) conn.close() + + +@pytest.mark.skipolddriver +def test_oauth_authorization_code_allows_empty_user(monkeypatch, omit_oauth_urls_check): + """Test that OAUTH_AUTHORIZATION_CODE authenticator allows connection without user parameter.""" + import snowflake.connector + + def mock_post_request(self, url, headers, json_body, **kwargs): + return { + "success": True, + "message": None, + "data": { + "token": "TOKEN", + "masterToken": "MASTER_TOKEN", + "idToken": None, + "parameters": [{"name": "SERVICE_NAME", "value": "FAKE_SERVICE_NAME"}], + }, + } + + monkeypatch.setattr( + snowflake.connector.network.SnowflakeRestful, "_post_request", mock_post_request + ) + + # Mock the OAuth authorization flow to avoid opening browser and starting HTTP server + def mock_request_tokens(self, **kwargs): + # Simulate successful token retrieval + return ("mock_access_token", "mock_refresh_token") + + monkeypatch.setattr(AuthByOauthCode, "_request_tokens", mock_request_tokens) + + # Test connection without user parameter - should succeed + conn = snowflake.connector.connect( + account="testaccount", + authenticator="OAUTH_AUTHORIZATION_CODE", + oauth_client_id="test_client_id", + oauth_client_secret="test_client_secret", + ) + + # Verify that the connection was successful + assert conn is not None + assert isinstance(conn.auth_class, AuthByOauthCode) + + conn.close() diff --git a/test/unit/test_auth_oauth_credentials.py b/test/unit/test_auth_oauth_credentials.py index 35cd037bd2..700962a9b1 100644 --- a/test/unit/test_auth_oauth_credentials.py +++ b/test/unit/test_auth_oauth_credentials.py @@ -137,6 +137,53 @@ def mock_get_request_token_response(self, connection, fields): conn.close() +@pytest.mark.skipolddriver +def test_oauth_client_credentials_allows_empty_user(monkeypatch): + """Test that OAUTH_CLIENT_CREDENTIALS authenticator allows connection without user parameter.""" + import snowflake.connector + + def mock_post_request(request, url, headers, json_body, **kwargs): + return { + "success": True, + "message": None, + "data": { + "token": "TOKEN", + "masterToken": "MASTER_TOKEN", + "idToken": None, + "parameters": [{"name": "SERVICE_NAME", "value": "FAKE_SERVICE_NAME"}], + }, + } + + monkeypatch.setattr( + "snowflake.connector.network.SnowflakeRestful._post_request", + mock_post_request, + ) + + # Mock the OAuth client credentials token request to avoid making HTTP requests + def mock_get_request_token_response(self, connection, fields): + return ("mocked_token_response", None) + + monkeypatch.setattr( + AuthByOauthCredentials, + "_get_request_token_response", + mock_get_request_token_response, + ) + + # Test connection without user parameter - should succeed + conn = snowflake.connector.connect( + account="testaccount", + authenticator="OAUTH_CLIENT_CREDENTIALS", + oauth_client_id="test_client_id", + oauth_client_secret="test_client_secret", + ) + + # Verify that the connection was successful + assert conn is not None + assert isinstance(conn.auth_class, AuthByOauthCredentials) + + conn.close() + + def test_oauth_credentials_missing_client_id_raises_error(): """Test that missing client_id raises a ProgrammingError.""" with pytest.raises(ProgrammingError) as excinfo: From 8bcbd838ee3f404d763d580ed4e11c738db74c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Wed, 12 Nov 2025 16:04:15 +0100 Subject: [PATCH 02/15] [async] Applied #2606 to async code --- .../aio/test_auth_oauth_auth_code_async.py | 48 ++++++++++++++++++ .../aio/test_auth_oauth_credentials_async.py | 50 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/test/unit/aio/test_auth_oauth_auth_code_async.py b/test/unit/aio/test_auth_oauth_auth_code_async.py index 091e0aa097..7152e4de8d 100644 --- a/test/unit/aio/test_auth_oauth_auth_code_async.py +++ b/test/unit/aio/test_auth_oauth_auth_code_async.py @@ -301,3 +301,51 @@ def test_mro(): assert AuthByOauthCode.mro().index(AuthByPluginAsync) < AuthByOauthCode.mro().index( AuthByPluginSync ) + + +@pytest.mark.skipolddriver +async def test_oauth_authorization_code_allows_empty_user( + monkeypatch, omit_oauth_urls_check +): + """Test that OAUTH_AUTHORIZATION_CODE authenticator allows connection without user parameter.""" + import snowflake.connector.aio + from snowflake.connector.aio._network import SnowflakeRestful + + async def mock_post_request(self, url, headers, json_body, **kwargs): + return { + "success": True, + "message": None, + "data": { + "token": "TOKEN", + "masterToken": "MASTER_TOKEN", + "idToken": None, + "parameters": [{"name": "SERVICE_NAME", "value": "FAKE_SERVICE_NAME"}], + }, + } + + monkeypatch.setattr(SnowflakeRestful, "_post_request", mock_post_request) + + # Mock the OAuth authorization flow to avoid opening browser and starting HTTP server + # Note: This must be a sync function (not async) because it's called from the sync + # parent class's prepare() method which calls _request_tokens() without await + def mock_request_tokens(self, **kwargs): + # Simulate successful token retrieval + return ("mock_access_token", "mock_refresh_token") + + monkeypatch.setattr(AuthByOauthCode, "_request_tokens", mock_request_tokens) + + # Test connection without user parameter - should succeed + conn = snowflake.connector.aio.SnowflakeConnection( + account="testaccount", + authenticator="OAUTH_AUTHORIZATION_CODE", + oauth_client_id="test_client_id", + oauth_client_secret="test_client_secret", + ) + + await conn.connect() + + # Verify that the connection was successful + assert conn is not None + assert isinstance(conn.auth_class, AuthByOauthCode) + + await conn.close() diff --git a/test/unit/aio/test_auth_oauth_credentials_async.py b/test/unit/aio/test_auth_oauth_credentials_async.py index aec07adf48..df955994bd 100644 --- a/test/unit/aio/test_auth_oauth_credentials_async.py +++ b/test/unit/aio/test_auth_oauth_credentials_async.py @@ -144,6 +144,56 @@ def mock_get_request_token_response(self, connection, fields): await conn.close() +@pytest.mark.skipolddriver +async def test_oauth_client_credentials_allows_empty_user(monkeypatch): + """Test that OAUTH_CLIENT_CREDENTIALS authenticator allows connection without user parameter.""" + import snowflake.connector.aio + + async def mock_post_request(self, url, headers, json_body, **kwargs): + return { + "success": True, + "message": None, + "data": { + "token": "TOKEN", + "masterToken": "MASTER_TOKEN", + "idToken": None, + "parameters": [{"name": "SERVICE_NAME", "value": "FAKE_SERVICE_NAME"}], + }, + } + + monkeypatch.setattr( + snowflake.connector.aio._network.SnowflakeRestful, + "_post_request", + mock_post_request, + ) + + # Mock the OAuth client credentials token request to avoid making HTTP requests + def mock_get_request_token_response(self, connection, fields): + return ("mocked_token_response", None) + + monkeypatch.setattr( + AuthByOauthCredentials, + "_get_request_token_response", + mock_get_request_token_response, + ) + + # Test connection without user parameter - should succeed + conn = snowflake.connector.aio.SnowflakeConnection( + account="testaccount", + authenticator="OAUTH_CLIENT_CREDENTIALS", + oauth_client_id="test_client_id", + oauth_client_secret="test_client_secret", + ) + + await conn.connect() + + # Verify that the connection was successful + assert conn is not None + assert isinstance(conn.auth_class, AuthByOauthCredentials) + + await conn.close() + + async def test_oauth_credentials_missing_client_id_raises_error(): """Test that missing client_id raises a ProgrammingError.""" with pytest.raises(ProgrammingError) as excinfo: From 5ad0f6bfcc380b62de63e22076bf126a7012ba87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Wed, 1 Oct 2025 14:11:32 +0200 Subject: [PATCH 03/15] Snow 1983343 add timeout for ocsp root certificates (#2559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Kubik (cherry picked from commit b301717582ce4543996206520e285c6ad86b640f) # Conflicts: # src/snowflake/connector/ssl_wrap_socket.py --- src/snowflake/connector/connection.py | 6 + src/snowflake/connector/constants.py | 3 + src/snowflake/connector/network.py | 7 ++ src/snowflake/connector/ocsp_snowflake.py | 131 ++++++++++++--------- src/snowflake/connector/ssl_wrap_socket.py | 6 +- test/integ/test_connection.py | 83 +++++++++++++ 6 files changed, 177 insertions(+), 59 deletions(-) diff --git a/src/snowflake/connector/connection.py b/src/snowflake/connector/connection.py index bfb0553752..7b35e07a99 100644 --- a/src/snowflake/connector/connection.py +++ b/src/snowflake/connector/connection.py @@ -70,6 +70,7 @@ _DOMAIN_NAME_MAP, _OAUTH_DEFAULT_SCOPE, ENV_VAR_PARTNER, + OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT, PARAMETER_AUTOCOMMIT, PARAMETER_CLIENT_PREFETCH_THREADS, PARAMETER_CLIENT_REQUEST_MFA_TOKEN, @@ -242,6 +243,10 @@ def _get_private_bytes_from_file( "internal_application_version": (CLIENT_VERSION, (type(None), str)), "disable_ocsp_checks": (False, bool), "ocsp_fail_open": (True, bool), # fail open on ocsp issues, default true + "ocsp_root_certs_dict_lock_timeout": ( + OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT, # no timeout + int, + ), "inject_client_pause": (0, int), # snowflake internal "session_parameters": (None, (type(None), dict)), # snowflake session parameters "autocommit": (None, (type(None), bool)), # snowflake @@ -443,6 +448,7 @@ class SnowflakeConnection: validates the TLS certificate but doesn't check revocation status with OCSP provider. ocsp_fail_open: Whether or not the connection is in fail open mode. Fail open mode decides if TLS certificates continue to be validated. Revoked certificates are blocked. Any other exceptions are disregarded. + ocsp_root_certs_dict_lock_timeout: Timeout for the OCSP root certs dict lock in seconds. Default value is -1, which means no timeout. session_id: The session ID of the connection. user: The user name used in the connection. host: The host name the connection attempts to connect to. diff --git a/src/snowflake/connector/constants.py b/src/snowflake/connector/constants.py index 17aaae8d56..47f07b9eb9 100644 --- a/src/snowflake/connector/constants.py +++ b/src/snowflake/connector/constants.py @@ -354,6 +354,9 @@ class FileHeader(NamedTuple): HTTP_HEADER_VALUE_OCTET_STREAM = "application/octet-stream" +# OCSP +OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT: int = -1 + @unique class OCSPMode(Enum): diff --git a/src/snowflake/connector/network.py b/src/snowflake/connector/network.py index ae34375a42..a8759ed2d2 100644 --- a/src/snowflake/connector/network.py +++ b/src/snowflake/connector/network.py @@ -41,6 +41,7 @@ HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_SERVICE_NAME, HTTP_HEADER_USER_AGENT, + OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT, ) from .description import ( CLIENT_NAME, @@ -337,6 +338,12 @@ def __init__( ssl_wrap_socket.FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME = ( self._connection._ocsp_response_cache_filename if self._connection else None ) + # OCSP root timeout + ssl_wrap_socket.FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT = ( + self._connection._ocsp_root_certs_dict_lock_timeout + if self._connection + else OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT + ) # This is to address the issue where requests hangs _ = "dummy".encode("idna").decode("utf-8") diff --git a/src/snowflake/connector/ocsp_snowflake.py b/src/snowflake/connector/ocsp_snowflake.py index d9cf8448ad..a8599c4ea1 100644 --- a/src/snowflake/connector/ocsp_snowflake.py +++ b/src/snowflake/connector/ocsp_snowflake.py @@ -58,6 +58,7 @@ from . import constants from .backoff_policies import exponential_backoff from .cache import CacheEntry, SFDictCache, SFDictFileCache +from .constants import OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT from .telemetry import TelemetryField, generate_telemetry_data_dict from .url_util import extract_top_level_domain_from_hostname, url_encode_str from .util_text import _base64_bytes_to_str @@ -1037,6 +1038,7 @@ def __init__( use_ocsp_cache_server=None, use_post_method: bool = True, use_fail_open: bool = True, + root_certs_dict_lock_timeout: int = OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT, **kwargs, ) -> None: self.test_mode = os.getenv("SF_OCSP_TEST_MODE", None) @@ -1045,6 +1047,7 @@ def __init__( logger.debug("WARNING - DRIVER CONFIGURED IN TEST MODE") self._use_post_method = use_post_method + self._root_certs_dict_lock_timeout = root_certs_dict_lock_timeout self.OCSP_CACHE_SERVER = OCSPServer( top_level_domain=extract_top_level_domain_from_hostname( kwargs.pop("hostname", None) @@ -1415,67 +1418,79 @@ def _check_ocsp_response_cache_server( def _lazy_read_ca_bundle(self) -> None: """Reads the local cabundle file and cache it in memory.""" - with SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK: - if SnowflakeOCSP.ROOT_CERTIFICATES_DICT: - # return if already loaded - return - + lock_acquired = SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK.acquire( + timeout=self._root_certs_dict_lock_timeout + ) + if lock_acquired: try: - ca_bundle = environ.get("REQUESTS_CA_BUNDLE") or environ.get( - "CURL_CA_BUNDLE" - ) - if ca_bundle and path.exists(ca_bundle): - # if the user/application specifies cabundle. - self.read_cert_bundle(ca_bundle) - else: - import sys - - # This import that depends on these libraries is to import certificates from them, - # we would like to have these as up to date as possible. - from requests import certs + if SnowflakeOCSP.ROOT_CERTIFICATES_DICT: + # return if already loaded + return - if ( - hasattr(certs, "__file__") - and path.exists(certs.__file__) - and path.exists( - path.join(path.dirname(certs.__file__), "cacert.pem") - ) - ): - # if cacert.pem exists next to certs.py in request - # package. - ca_bundle = path.join( - path.dirname(certs.__file__), "cacert.pem" - ) + try: + ca_bundle = environ.get("REQUESTS_CA_BUNDLE") or environ.get( + "CURL_CA_BUNDLE" + ) + if ca_bundle and path.exists(ca_bundle): + # if the user/application specifies cabundle. self.read_cert_bundle(ca_bundle) - elif hasattr(sys, "_MEIPASS"): - # if pyinstaller includes cacert.pem - cabundle_candidates = [ - ["botocore", "vendored", "requests", "cacert.pem"], - ["requests", "cacert.pem"], - ["cacert.pem"], - ] - for filename in cabundle_candidates: - ca_bundle = path.join(sys._MEIPASS, *filename) - if path.exists(ca_bundle): - self.read_cert_bundle(ca_bundle) - break - else: - logger.error("No cabundle file is found in _MEIPASS") - try: - import certifi - - self.read_cert_bundle(certifi.where()) - except Exception: - logger.debug("no certifi is installed. ignored.") - - except Exception as e: - logger.error("Failed to read ca_bundle: %s", e) - - if not SnowflakeOCSP.ROOT_CERTIFICATES_DICT: - logger.error( - "No CA bundle file is found in the system. " - "Set REQUESTS_CA_BUNDLE to the file." - ) + else: + import sys + + # This import that depends on these libraries is to import certificates from them, + # we would like to have these as up to date as possible. + from requests import certs + + if ( + hasattr(certs, "__file__") + and path.exists(certs.__file__) + and path.exists( + path.join(path.dirname(certs.__file__), "cacert.pem") + ) + ): + # if cacert.pem exists next to certs.py in request + # package. + ca_bundle = path.join( + path.dirname(certs.__file__), "cacert.pem" + ) + self.read_cert_bundle(ca_bundle) + elif hasattr(sys, "_MEIPASS"): + # if pyinstaller includes cacert.pem + cabundle_candidates = [ + ["botocore", "vendored", "requests", "cacert.pem"], + ["requests", "cacert.pem"], + ["cacert.pem"], + ] + for filename in cabundle_candidates: + ca_bundle = path.join(sys._MEIPASS, *filename) + if path.exists(ca_bundle): + self.read_cert_bundle(ca_bundle) + break + else: + logger.error("No cabundle file is found in _MEIPASS") + try: + import certifi + + self.read_cert_bundle(certifi.where()) + except Exception: + logger.debug("no certifi is installed. ignored.") + + except Exception as e: + logger.error("Failed to read ca_bundle: %s", e) + + if not SnowflakeOCSP.ROOT_CERTIFICATES_DICT: + logger.error( + "No CA bundle file is found in the system. " + "Set REQUESTS_CA_BUNDLE to the file." + ) + finally: + SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK.release() + else: + logger.info( + "Failed to acquire lock for ROOT_CERTIFICATES_DICT_LOCK. " + "Skipping reading CA bundle." + ) + return @staticmethod def _calculate_tolerable_validity(this_update: float, next_update: float) -> int: diff --git a/src/snowflake/connector/ssl_wrap_socket.py b/src/snowflake/connector/ssl_wrap_socket.py index f1a14e5c89..8f3542d7b3 100644 --- a/src/snowflake/connector/ssl_wrap_socket.py +++ b/src/snowflake/connector/ssl_wrap_socket.py @@ -21,7 +21,7 @@ import certifi import OpenSSL.SSL -from .constants import OCSPMode +from .constants import OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT, OCSPMode from .errorcode import ER_OCSP_RESPONSE_CERT_STATUS_REVOKED from .errors import OperationalError from .session_manager import SessionManager @@ -31,6 +31,9 @@ DEFAULT_OCSP_MODE: OCSPMode = OCSPMode.FAIL_OPEN FEATURE_OCSP_MODE: OCSPMode = DEFAULT_OCSP_MODE +FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT: int = ( + OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT +) """ OCSP Response cache file name @@ -179,6 +182,7 @@ def ssl_wrap_socket_with_ocsp(*args: Any, **kwargs: Any) -> WrappedSocket: ocsp_response_cache_uri=FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME, use_fail_open=FEATURE_OCSP_MODE == OCSPMode.FAIL_OPEN, hostname=server_hostname, + root_certs_dict_lock_timeout=FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT, ).validate(server_hostname, ret.connection) if not v: raise OperationalError( diff --git a/test/integ/test_connection.py b/test/integ/test_connection.py index 966c8f7d10..7f4a54834f 100644 --- a/test/integ/test_connection.py +++ b/test/integ/test_connection.py @@ -12,6 +12,7 @@ import warnings import weakref from unittest import mock +from unittest.mock import MagicMock, PropertyMock, patch from uuid import uuid4 import pytest @@ -1585,6 +1586,88 @@ def test_ocsp_mode_insecure_mode_and_disable_ocsp_checks_mismatch_ocsp_enabled( assert "snowflake.connector.ocsp_snowflake" not in caplog.text +@pytest.mark.skipolddriver +def test_root_certs_dict_lock_timeout_fail_open(conn_cnx): + """Test OCSP root certificates lock timeout with fail-open mode and side effect mock.""" + + override_config = { + "ocsp_fail_open": True, + "ocsp_root_certs_dict_lock_timeout": 0.1, + } + + with patch( + "snowflake.connector.ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" + ) as mock_lock: + snowflake.connector.ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = {} + + mock_lock.acquire = MagicMock(return_value=False) + mock_lock.release = MagicMock() + + with conn_cnx(**override_config) as conn: + try: + with conn.cursor() as cur: + assert cur.execute("select 1").fetchall() == [(1,)] + + if mock_lock.acquire.called: + mock_lock.acquire.assert_called_with(timeout=0.1) + assert conn._ocsp_root_certs_dict_lock_timeout == 0.1 + finally: + conn.close() + + +@pytest.mark.skipolddriver +@pytest.mark.parametrize( + "ocsp_fail_open,timeout_value,expected_timeout", + [ + (False, 1, 1), # fail-close mode with 1 second timeout + (True, 2, 2), # fail-open mode with 2 second timeout + ], +) +def test_root_certs_dict_lock_timeout_with_property_mock( + conn_cnx, ocsp_fail_open, timeout_value, expected_timeout +): + """Test OCSP root certificates lock timeout with property mock for different configurations.""" + config = { + "ocsp_fail_open": ocsp_fail_open, + "ocsp_root_certs_dict_lock_timeout": timeout_value, + } + + with patch( + "snowflake.connector.ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" + ) as mock_lock: + snowflake.connector.ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = {} + + type(mock_lock).acquire = PropertyMock(return_value=lambda timeout: False) + type(mock_lock).release = PropertyMock(return_value=lambda: None) + + with conn_cnx(**config) as conn: + with conn.cursor() as cur: + assert cur.execute("select 1").fetchall() == [(1,)] + + assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout + conn.close() + + +@pytest.mark.skipolddriver +@pytest.mark.parametrize( + "config,expected_timeout", + [ + ({"ocsp_fail_open": True, "ocsp_root_certs_dict_lock_timeout": 0.001}, 0.001), + ({"ocsp_fail_open": True}, -1), # no timeout specified, should default to -1 + ], +) +def test_root_certs_dict_lock_timeout_basic_config(conn_cnx, config, expected_timeout): + """Test OCSP root certificates lock timeout basic configuration without mocking.""" + with conn_cnx(**config) as conn: + try: + with conn.cursor() as cur: + assert cur.execute("select 1").fetchall() == [(1,)] + + assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout + finally: + conn.close() + + @pytest.mark.skipolddriver def test_ocsp_mode_insecure_mode_deprecation_warning(conn_cnx): with warnings.catch_warnings(record=True) as w: From 3fd205baca03de6b8a1f440be0e189852b5b9ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Wed, 12 Nov 2025 21:18:25 +0100 Subject: [PATCH 04/15] [async] Applied #2559 to async code --- .../connector/aio/_ocsp_snowflake.py | 8 +- .../connector/aio/_session_manager.py | 10 ++- test/integ/aio_it/test_connection_async.py | 88 +++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/snowflake/connector/aio/_ocsp_snowflake.py b/src/snowflake/connector/aio/_ocsp_snowflake.py index f16cf467e5..3165e54b11 100644 --- a/src/snowflake/connector/aio/_ocsp_snowflake.py +++ b/src/snowflake/connector/aio/_ocsp_snowflake.py @@ -25,7 +25,11 @@ ) from snowflake.connector.errors import RevocationCheckError from snowflake.connector.network import PYTHON_CONNECTOR_USER_AGENT -from snowflake.connector.ocsp_snowflake import OCSPCache, OCSPResponseValidationResult +from snowflake.connector.ocsp_snowflake import ( + OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT, + OCSPCache, + OCSPResponseValidationResult, +) from snowflake.connector.ocsp_snowflake import OCSPServer as OCSPServerSync from snowflake.connector.ocsp_snowflake import OCSPTelemetryData from snowflake.connector.ocsp_snowflake import SnowflakeOCSP as SnowflakeOCSPSync @@ -143,6 +147,7 @@ def __init__( use_ocsp_cache_server=None, use_post_method: bool = True, use_fail_open: bool = True, + root_certs_dict_lock_timeout: int = OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT, **kwargs, ) -> None: self.test_mode = os.getenv("SF_OCSP_TEST_MODE", None) @@ -151,6 +156,7 @@ def __init__( logger.debug("WARNING - DRIVER CONFIGURED IN TEST MODE") self._use_post_method = use_post_method + self._root_certs_dict_lock_timeout = root_certs_dict_lock_timeout self.OCSP_CACHE_SERVER = OCSPServer( top_level_domain=extract_top_level_domain_from_hostname( kwargs.pop("hostname", None) diff --git a/src/snowflake/connector/aio/_session_manager.py b/src/snowflake/connector/aio/_session_manager.py index a2fa06bc0a..09d6155dfc 100644 --- a/src/snowflake/connector/aio/_session_manager.py +++ b/src/snowflake/connector/aio/_session_manager.py @@ -11,7 +11,7 @@ from .. import OperationalError from ..errorcode import ER_OCSP_RESPONSE_CERT_STATUS_REVOKED -from ..ssl_wrap_socket import FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME +from ..ssl_wrap_socket import FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME, FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT from ._ocsp_asn1crypto import SnowflakeOCSPAsn1Crypto if TYPE_CHECKING: @@ -102,6 +102,7 @@ async def validate_ocsp( ocsp_response_cache_uri=FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME, use_fail_open=self._snowflake_ocsp_mode == OCSPMode.FAIL_OPEN, hostname=hostname, + root_certs_dict_lock_timeout=FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT, ).validate(hostname, protocol, session_manager=session_manager) if not v: raise OperationalError( @@ -223,8 +224,13 @@ async def get( use_pooling: bool | None = None, **kwargs, ) -> aiohttp.ClientResponse: - async with self.use_session(url, use_pooling) as session: + if isinstance(timeout, tuple): + connect, total = timeout + timeout_obj = aiohttp.ClientTimeout(total=total, connect=connect) + else: timeout_obj = aiohttp.ClientTimeout(total=timeout) if timeout else None + + async with self.use_session(url, use_pooling) as session: return await session.get( url, headers=headers, timeout=timeout_obj, **kwargs ) diff --git a/test/integ/aio_it/test_connection_async.py b/test/integ/aio_it/test_connection_async.py index de2497820f..ff7bd699ec 100644 --- a/test/integ/aio_it/test_connection_async.py +++ b/test/integ/aio_it/test_connection_async.py @@ -1335,6 +1335,94 @@ async def test_ocsp_mode_insecure_mode_and_disable_ocsp_checks_mismatch_ocsp_ena assert "snowflake.connector.aio._ocsp_snowflake" not in caplog.text +@pytest.mark.skipolddriver +async def test_root_certs_dict_lock_timeout_fail_open(conn_cnx): + """Test OCSP root certificates lock timeout with fail-open mode and side effect mock.""" + + override_config = { + "ocsp_fail_open": True, + "ocsp_root_certs_dict_lock_timeout": 0.1, + } + + with mock.patch( + "snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" + ) as mock_lock: + snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = ( + {} + ) + + mock_lock.acquire = mock.MagicMock(return_value=False) + mock_lock.release = mock.MagicMock() + + async with conn_cnx(**override_config) as conn: + try: + async with conn.cursor() as cur: + assert await (await cur.execute("select 1")).fetchall() == [(1,)] + + if mock_lock.acquire.called: + mock_lock.acquire.assert_called_with(timeout=0.1) + assert conn._ocsp_root_certs_dict_lock_timeout == 0.1 + finally: + await conn.close() + + +@pytest.mark.skipolddriver +@pytest.mark.parametrize( + "ocsp_fail_open,timeout_value,expected_timeout", + [ + (False, 1, 1), # fail-close mode with 1 second timeout + (True, 2, 2), # fail-open mode with 2 second timeout + ], +) +async def test_root_certs_dict_lock_timeout_with_property_mock( + conn_cnx, ocsp_fail_open, timeout_value, expected_timeout +): + """Test OCSP root certificates lock timeout with property mock for different configurations.""" + config = { + "ocsp_fail_open": ocsp_fail_open, + "ocsp_root_certs_dict_lock_timeout": timeout_value, + } + + with mock.patch( + "snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" + ) as mock_lock: + snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = ( + {} + ) + + type(mock_lock).acquire = mock.PropertyMock(return_value=lambda timeout: False) + type(mock_lock).release = mock.PropertyMock(return_value=lambda: None) + + async with conn_cnx(**config) as conn: + async with conn.cursor() as cur: + assert await (await cur.execute("select 1")).fetchall() == [(1,)] + + assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout + await conn.close() + + +@pytest.mark.skipolddriver +@pytest.mark.parametrize( + "config,expected_timeout", + [ + ({"ocsp_fail_open": True, "ocsp_root_certs_dict_lock_timeout": 0.001}, 0.001), + ({"ocsp_fail_open": True}, -1), # no timeout specified, should default to -1 + ], +) +async def test_root_certs_dict_lock_timeout_basic_config( + conn_cnx, config, expected_timeout +): + """Test OCSP root certificates lock timeout basic configuration without mocking.""" + async with conn_cnx(**config) as conn: + try: + async with conn.cursor() as cur: + assert await (await cur.execute("select 1")).fetchall() == [(1,)] + + assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout + finally: + await conn.close() + + @pytest.mark.skipolddriver async def test_ocsp_mode_insecure_mode_deprecation_warning(conn_cnx): with warnings.catch_warnings(record=True) as w: From 56018ef9bdacb20efae33d0984541be83e603f29 Mon Sep 17 00:00:00 2001 From: Mathias Florin <31008003+mathiasflorin@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:40:01 +0200 Subject: [PATCH 05/15] Improve AWS region detection by checking AWS_DEFAULT_REGION as fallback (#2535) Co-authored-by: Patryk Czajka (cherry picked from commit ae2bf2e6f76bc92597153692752a6b7488961de9) --- src/snowflake/connector/wif_util.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/snowflake/connector/wif_util.py b/src/snowflake/connector/wif_util.py index 28069b3591..0ba84ccab6 100644 --- a/src/snowflake/connector/wif_util.py +++ b/src/snowflake/connector/wif_util.py @@ -96,10 +96,11 @@ def extract_iss_and_sub_without_signature_verification(jwt_str: str) -> tuple[st def get_aws_region() -> str: """Get the current AWS workload's region, or raises an error if it's missing.""" - region = None - if "AWS_REGION" in os.environ: # Lambda - region = os.environ["AWS_REGION"] - else: # EC2 + + region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") + + if not region: + # Fallback for EC2 environments # TODO: SNOW-2223669 Investigate if our adapters - containing settings of http traffic - should be passed here as boto urllib3session. Those requests go to local servers, so they do not need Proxy setup or Headers customization in theory. But we may want to have all the traffic going through one class (e.g. Adapter or mixin). region = InstanceMetadataRegionFetcher().retrieve_region() @@ -108,6 +109,7 @@ def get_aws_region() -> str: msg="No AWS region was found. Ensure the application is running on AWS.", errno=ER_WIF_CREDENTIALS_NOT_FOUND, ) + return region From d91631cd7b4a306bf97d28a138032c14a6e7ae05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Wed, 12 Nov 2025 21:20:36 +0100 Subject: [PATCH 06/15] [async] Applied #2535 to async code --- src/snowflake/connector/aio/_wif_util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/snowflake/connector/aio/_wif_util.py b/src/snowflake/connector/aio/_wif_util.py index b7aa7ad6ff..defeef8002 100644 --- a/src/snowflake/connector/aio/_wif_util.py +++ b/src/snowflake/connector/aio/_wif_util.py @@ -34,9 +34,10 @@ async def get_aws_region() -> str: """Get the current AWS workload's region.""" - if "AWS_REGION" in os.environ: # Lambda - region = os.environ["AWS_REGION"] - else: # EC2 + region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") + + if not region: + # Fallback for EC2 environments region = ( await aiobotocore.utils.AioInstanceMetadataRegionFetcher().retrieve_region() ) From ab7a1b929064ed4b0c6da09a42cb7405dc8447bb Mon Sep 17 00:00:00 2001 From: George Merticariu <103256710+sfc-gh-gmerticariu@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:22:30 +0200 Subject: [PATCH 07/15] SNOW-2161716: Raise error if the config file is writable by others (#2501) Co-authored-by: Patryk Czajka (cherry picked from commit e3349a3f425f74e6018d006ad1dbb701eca02b6c) --- src/snowflake/connector/config_manager.py | 14 +++++- test/unit/test_configmanager.py | 54 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/snowflake/connector/config_manager.py b/src/snowflake/connector/config_manager.py index 83ec493b77..2ec167ac38 100644 --- a/src/snowflake/connector/config_manager.py +++ b/src/snowflake/connector/config_manager.py @@ -27,7 +27,7 @@ LOGGER = logging.getLogger(__name__) READABLE_BY_OTHERS = stat.S_IRGRP | stat.S_IROTH - +WRITABLE_BY_OTHERS = stat.S_IWGRP | stat.S_IWOTH SKIP_WARNING_ENV_VAR = "SF_SKIP_WARNING_FOR_READ_PERMISSIONS_ON_CONFIG_FILE" @@ -337,6 +337,18 @@ def read_config( ) continue + # Check for writable by others - this should raise an error + if ( + not IS_WINDOWS # Skip checking on Windows + and sliceoptions.check_permissions # Skip checking if this file couldn't hold sensitive information + and filep.stat().st_mode & WRITABLE_BY_OTHERS != 0 + ): + file_stat = filep.stat() + file_permissions = oct(file_stat.st_mode)[-3:] + raise ConfigSourceError( + f"file '{str(filep)}' is writable by group or others — this poses a security risk because it allows unauthorized users to modify sensitive settings. Your Permission: {file_permissions}" + ) + # Check for readable by others or wrong ownership - this should warn if ( not IS_WINDOWS # Skip checking on Windows diff --git a/test/unit/test_configmanager.py b/test/unit/test_configmanager.py index 08ca62faf9..5bd8be6c92 100644 --- a/test/unit/test_configmanager.py +++ b/test/unit/test_configmanager.py @@ -639,6 +639,60 @@ def test_log_debug_config_file_parent_dir_permissions(tmp_path, caplog): shutil.rmtree(tmp_dir) +@pytest.mark.skipif(IS_WINDOWS, reason="chmod doesn't work on Windows") +def test_error_config_file_writable_by_others(tmp_path): + c_file = tmp_path / "config.toml" + c1 = ConfigManager(file_path=c_file, name="root_parser") + c1.add_option(name="b", parse_str=lambda e: e.lower() == "true") + c_file.write_text( + dedent( + """\ + b = true + """ + ) + ) + # Make file writable by others + c_file.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IWOTH) + file_permissions = oct(c_file.stat().st_mode)[-3:] + + with pytest.raises( + ConfigSourceError, + match=re.escape( + f"file '{str(c_file)}' is writable by group or others — this poses a security risk because it allows unauthorized users to modify sensitive settings. Your Permission: {file_permissions}" + ), + ): + c1["b"] + + +@pytest.mark.skipif(IS_WINDOWS, reason="chmod doesn't work on Windows") +def test_error_config_file_writable_by_group(tmp_path): + c_file = tmp_path / "config.toml" + c1 = ConfigManager(file_path=c_file, name="root_parser") + c1.add_option(name="b", parse_str=lambda e: e.lower() == "true") + c_file.write_text( + dedent( + """\ + b = true + """ + ) + ) + # Make file writable by group + c_file.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IWGRP) + file_permissions = oct(c_file.stat().st_mode)[-3:] + + with pytest.raises( + ConfigSourceError, + match=re.escape( + f"file '{str(c_file)}' is writable by group or others — this poses a security risk because it allows unauthorized users to modify sensitive settings. Your Permission: {file_permissions}" + ), + ): + c1["b"] + + # file permissions check can be skipped with unsafe_skip_file_permissions_check flag + c1.read_config(skip_file_permissions_check=True) + assert c1["b"] is True + + @pytest.mark.skipif(IS_WINDOWS, reason="chmod doesn't work on Windows") def test_skip_warning_config_file_permissions(tmp_path, monkeypatch): c_file = tmp_path / "config.toml" From e232968e5b9757e6c1c82b2db5a2e22e6621ebbd Mon Sep 17 00:00:00 2001 From: Patryk Czajka Date: Wed, 1 Oct 2025 15:56:55 +0200 Subject: [PATCH 08/15] Bumped up PythonConnector version from 3.17.4 to 4.0.0 (#2561) Co-authored-by: Jenkins User <900904> Co-authored-by: github-actions (cherry picked from commit d6113bad0f0949d98ad15c38c06f8c3290288580) --- src/snowflake/connector/version.py | 2 +- tested_requirements/requirements_310.reqs | 22 ++++++++++----------- tested_requirements/requirements_311.reqs | 22 ++++++++++----------- tested_requirements/requirements_312.reqs | 22 ++++++++++----------- tested_requirements/requirements_313.reqs | 24 +++++++++++------------ tested_requirements/requirements_39.reqs | 22 ++++++++++----------- 6 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/snowflake/connector/version.py b/src/snowflake/connector/version.py index 1ef56b4592..ca224e829a 100644 --- a/src/snowflake/connector/version.py +++ b/src/snowflake/connector/version.py @@ -1,3 +1,3 @@ # Update this for the versions # Don't change the forth version number from None -VERSION = (3, 17, 1, None) +VERSION = (4, 0, 0, None) diff --git a/tested_requirements/requirements_310.reqs b/tested_requirements/requirements_310.reqs index 06103b858b..328b315052 100644 --- a/tested_requirements/requirements_310.reqs +++ b/tested_requirements/requirements_310.reqs @@ -1,26 +1,26 @@ # Generated on: Python 3.10.18 asn1crypto==1.5.1 -boto3==1.40.9 -botocore==1.40.9 +boto3==1.40.42 +botocore==1.40.42 certifi==2025.8.3 -cffi==1.17.1 +cffi==2.0.0 charset-normalizer==3.4.3 -cryptography==45.0.6 +cryptography==46.0.2 filelock==3.19.1 idna==3.10 jmespath==1.0.1 packaging==25.0 -platformdirs==4.3.8 -pycparser==2.22 +platformdirs==4.4.0 +pycparser==2.23 PyJWT==2.10.1 -pyOpenSSL==25.1.0 +pyOpenSSL==25.3.0 python-dateutil==2.9.0.post0 pytz==2025.2 -requests==2.32.4 -s3transfer==0.13.1 +requests==2.32.5 +s3transfer==0.14.0 six==1.17.0 sortedcontainers==2.4.0 tomlkit==0.13.3 -typing_extensions==4.14.1 +typing_extensions==4.15.0 urllib3==2.5.0 -snowflake-connector-python==3.17.1 +snowflake-connector-python==4.0.0 diff --git a/tested_requirements/requirements_311.reqs b/tested_requirements/requirements_311.reqs index f8b0aefd78..01d0767cf4 100644 --- a/tested_requirements/requirements_311.reqs +++ b/tested_requirements/requirements_311.reqs @@ -1,26 +1,26 @@ # Generated on: Python 3.11.13 asn1crypto==1.5.1 -boto3==1.40.9 -botocore==1.40.9 +boto3==1.40.42 +botocore==1.40.42 certifi==2025.8.3 -cffi==1.17.1 +cffi==2.0.0 charset-normalizer==3.4.3 -cryptography==45.0.6 +cryptography==46.0.2 filelock==3.19.1 idna==3.10 jmespath==1.0.1 packaging==25.0 -platformdirs==4.3.8 -pycparser==2.22 +platformdirs==4.4.0 +pycparser==2.23 PyJWT==2.10.1 -pyOpenSSL==25.1.0 +pyOpenSSL==25.3.0 python-dateutil==2.9.0.post0 pytz==2025.2 -requests==2.32.4 -s3transfer==0.13.1 +requests==2.32.5 +s3transfer==0.14.0 six==1.17.0 sortedcontainers==2.4.0 tomlkit==0.13.3 -typing_extensions==4.14.1 +typing_extensions==4.15.0 urllib3==2.5.0 -snowflake-connector-python==3.17.1 +snowflake-connector-python==4.0.0 diff --git a/tested_requirements/requirements_312.reqs b/tested_requirements/requirements_312.reqs index 8b2ab33ec3..a1eac2c622 100644 --- a/tested_requirements/requirements_312.reqs +++ b/tested_requirements/requirements_312.reqs @@ -1,28 +1,28 @@ # Generated on: Python 3.12.11 asn1crypto==1.5.1 -boto3==1.40.9 -botocore==1.40.9 +boto3==1.40.42 +botocore==1.40.42 certifi==2025.8.3 -cffi==1.17.1 +cffi==2.0.0 charset-normalizer==3.4.3 -cryptography==45.0.6 +cryptography==46.0.2 filelock==3.19.1 idna==3.10 jmespath==1.0.1 packaging==25.0 -platformdirs==4.3.8 -pycparser==2.22 +platformdirs==4.4.0 +pycparser==2.23 PyJWT==2.10.1 -pyOpenSSL==25.1.0 +pyOpenSSL==25.3.0 python-dateutil==2.9.0.post0 pytz==2025.2 -requests==2.32.4 -s3transfer==0.13.1 +requests==2.32.5 +s3transfer==0.14.0 setuptools==80.9.0 six==1.17.0 sortedcontainers==2.4.0 tomlkit==0.13.3 -typing_extensions==4.14.1 +typing_extensions==4.15.0 urllib3==2.5.0 wheel==0.45.1 -snowflake-connector-python==3.17.1 +snowflake-connector-python==4.0.0 diff --git a/tested_requirements/requirements_313.reqs b/tested_requirements/requirements_313.reqs index 50231c97df..da1b858552 100644 --- a/tested_requirements/requirements_313.reqs +++ b/tested_requirements/requirements_313.reqs @@ -1,28 +1,28 @@ -# Generated on: Python 3.13.5 +# Generated on: Python 3.13.7 asn1crypto==1.5.1 -boto3==1.40.9 -botocore==1.40.9 +boto3==1.40.42 +botocore==1.40.42 certifi==2025.8.3 -cffi==1.17.1 +cffi==2.0.0 charset-normalizer==3.4.3 -cryptography==45.0.6 +cryptography==46.0.2 filelock==3.19.1 idna==3.10 jmespath==1.0.1 packaging==25.0 -platformdirs==4.3.8 -pycparser==2.22 +platformdirs==4.4.0 +pycparser==2.23 PyJWT==2.10.1 -pyOpenSSL==25.1.0 +pyOpenSSL==25.3.0 python-dateutil==2.9.0.post0 pytz==2025.2 -requests==2.32.4 -s3transfer==0.13.1 +requests==2.32.5 +s3transfer==0.14.0 setuptools==80.9.0 six==1.17.0 sortedcontainers==2.4.0 tomlkit==0.13.3 -typing_extensions==4.14.1 +typing_extensions==4.15.0 urllib3==2.5.0 wheel==0.45.1 -snowflake-connector-python==3.17.1 +snowflake-connector-python==4.0.0 diff --git a/tested_requirements/requirements_39.reqs b/tested_requirements/requirements_39.reqs index 98815b9129..92772a670c 100644 --- a/tested_requirements/requirements_39.reqs +++ b/tested_requirements/requirements_39.reqs @@ -1,26 +1,26 @@ # Generated on: Python 3.9.23 asn1crypto==1.5.1 -boto3==1.40.9 -botocore==1.40.9 +boto3==1.40.42 +botocore==1.40.42 certifi==2025.8.3 -cffi==1.17.1 +cffi==2.0.0 charset-normalizer==3.4.3 -cryptography==45.0.6 +cryptography==46.0.2 filelock==3.19.1 idna==3.10 jmespath==1.0.1 packaging==25.0 -platformdirs==4.3.8 -pycparser==2.22 +platformdirs==4.4.0 +pycparser==2.23 PyJWT==2.10.1 -pyOpenSSL==25.1.0 +pyOpenSSL==25.3.0 python-dateutil==2.9.0.post0 pytz==2025.2 -requests==2.32.4 -s3transfer==0.13.1 +requests==2.32.5 +s3transfer==0.14.0 six==1.17.0 sortedcontainers==2.4.0 tomlkit==0.13.3 -typing_extensions==4.14.1 +typing_extensions==4.15.0 urllib3==1.26.20 -snowflake-connector-python==3.17.1 +snowflake-connector-python==4.0.0 From 5290fb914584356d489f6e2330f28ed5129259a7 Mon Sep 17 00:00:00 2001 From: David Szmolka <69192509+sfc-gh-dszmolka@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:53:47 +0200 Subject: [PATCH 09/15] SNOW-2324060 don't attempt non working bucket accelerate endpoint for internal stages (#2556) (cherry picked from commit 7708f1b20d356867bab2d83779d79f7bf1ad1e01) --- DESCRIPTION.md | 22 +++++++++++++++++++- src/snowflake/connector/s3_storage_client.py | 15 ++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 58fc1bee3d..5558302bbe 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -7,11 +7,31 @@ https://docs.snowflake.com/ Source code is also available at: https://github.com/snowflakedb/snowflake-connector-python # Release Notes -- v3.18.0(TBD) +- v4.1.0(TBD) + - Added `CERT_REVOCATION_CHECK_MODE` to `CLIENT_ENVIRONMENT` + +- v4.0.0(October 01,2025) + - Added support for checking certificates revocation using revocation lists (CRLs) - Added the `workload_identity_impersonation_path` parameter to support service account impersonation for Workload Identity Federation on GCP and AWS workloads only - Fixed `get_results_from_sfqid` when using `DictCursor` and executing multiple statements at once - Added the `oauth_credentials_in_body` parameter supporting an option to send the oauth client credentials in the request body + - Fix retry behavior for `ECONNRESET` error + - Added an option to exclude `botocore` and `boto3` dependencies by setting `SNOWFLAKE_NO_BOTO` environment variable during installation + - Revert changing exception type in case of token expired scenario for `Oauth` authenticator back to `DatabaseError` + - Enhanced configuration file security checks with stricter permission validation. + - Configuration files writable by group or others now raise a `ConfigSourceError` with detailed permission information, preventing potential credential tampering. + - Added support for pandas conversion for Day-time and Year-Month Interval types + - Fixed the return type of `SnowflakeConnection.cursor(cursor_class)` to match the type of `cursor_class` + - Constrained the types of `fetchone`, `fetchmany`, `fetchall` + - As part of this fix, `DictCursor` is no longer a subclass of `SnowflakeCursor`; use `SnowflakeCursorBase` as a superclass of both. + - Fix "No AWS region was found" error if AWS region was set in `AWS_DEFAULT_REGION` variable instead of `AWS_REGION` for `WORKLOAD_IDENTITY` authenticator + - Add `ocsp_root_certs_dict_lock_timeout` connection parameter to set the timeout (in seconds) for acquiring the lock on the OCSP root certs dictionary. Default value for this parameter is -1 which indicates no timeout. + - Fixed behaviour of trying S3 Transfer Accelerate endpoint by default for internal stages, and always getting HTTP403 due to permissions missing on purpose. Now /accelerate is not attempted. + +- v3.17.4(September 22,2025) - Added support for intermediate certificates as roots when they are stored in the trust store + - Bumped up vendored `urllib3` to `2.5.0` and `requests` to `v2.32.5` + - Dropped support for OpenSSL versions older than 1.1.1 - v3.17.3(September 02,2025) - Enhanced configuration file permission warning messages. diff --git a/src/snowflake/connector/s3_storage_client.py b/src/snowflake/connector/s3_storage_client.py index d2e49389d1..e19967dda1 100644 --- a/src/snowflake/connector/s3_storage_client.py +++ b/src/snowflake/connector/s3_storage_client.py @@ -120,9 +120,17 @@ def transfer_accelerate_config( return False else: if use_accelerate_endpoint is None: - use_accelerate_endpoint = self._get_bucket_accelerate_config( - self.s3location.bucket_name - ) + if str(self.s3location.bucket_name).lower().startswith("sfc-"): + # SNOW-2324060: no s3:GetAccelerateConfiguration and no intention to add either + # for internal stage, thus previously the client got HTTP403 on /accelerate call + logger.debug( + "Not attempting to get bucket transfer accelerate endpoint for internal stage." + ) + use_accelerate_endpoint = False + else: + use_accelerate_endpoint = self._get_bucket_accelerate_config( + self.s3location.bucket_name + ) if use_accelerate_endpoint: self.endpoint = ( @@ -132,6 +140,7 @@ def transfer_accelerate_config( self.endpoint = ( f"https://{self.s3location.bucket_name}.s3.amazonaws.com" ) + logger.debug(f"Using {self.endpoint} as storage endpoint.") return use_accelerate_endpoint @staticmethod From aac8852db15ffc222b85cf84aca80775c82abd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Wed, 12 Nov 2025 21:25:27 +0100 Subject: [PATCH 10/15] [async] Applied #2556 to async code --- src/snowflake/connector/aio/_s3_storage_client.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/snowflake/connector/aio/_s3_storage_client.py b/src/snowflake/connector/aio/_s3_storage_client.py index 371fa50e71..94c522c0d1 100644 --- a/src/snowflake/connector/aio/_s3_storage_client.py +++ b/src/snowflake/connector/aio/_s3_storage_client.py @@ -401,9 +401,17 @@ async def transfer_accelerate_config( return False else: if use_accelerate_endpoint is None: - use_accelerate_endpoint = await self._get_bucket_accelerate_config( - self.s3location.bucket_name - ) + if str(self.s3location.bucket_name).lower().startswith("sfc-"): + # SNOW-2324060: no s3:GetAccelerateConfiguration and no intention to add either + # for internal stage, thus previously the client got HTTP403 on /accelerate call + logger.debug( + "Not attempting to get bucket transfer accelerate endpoint for internal stage." + ) + use_accelerate_endpoint = False + else: + use_accelerate_endpoint = await self._get_bucket_accelerate_config( + self.s3location.bucket_name + ) if use_accelerate_endpoint: self.endpoint = ( @@ -413,6 +421,7 @@ async def transfer_accelerate_config( self.endpoint = ( f"https://{self.s3location.bucket_name}.s3.amazonaws.com" ) + logger.debug(f"Using {self.endpoint} as storage endpoint.") return use_accelerate_endpoint async def _has_expired_token(self, response: aiohttp.ClientResponse) -> bool: From 63e7d41426536b40a95b1cfcc61a1948b8a4cacf Mon Sep 17 00:00:00 2001 From: Patryk Cyrek Date: Wed, 8 Oct 2025 11:54:52 +0200 Subject: [PATCH 11/15] SNOW-2021009: Improving CICD, flakiness fixes (#2569) (cherry picked from commit cfa0ad8045cae8317fc1eda93e25592a589f2115) --- .github/workflows/build_test.yml | 47 +++++++++++++++++++++++--------- tox.ini | 2 +- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index f04a96c1c9..2adccdfb84 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -19,6 +19,14 @@ on: required: true tags: description: "Test scenario tags" + required: false + type: string + +permissions: + contents: read + actions: read + checks: write + pull-requests: write concurrency: # older builds for the same pull request number or branch should be cancelled @@ -223,7 +231,6 @@ jobs: # To run a single test on GHA use the below command: # run: python -m tox run -e `echo py${PYTHON_VERSION/\./}-single-ci | sed 's/ /,/g'` run: python -m tox run -e `echo py${PYTHON_VERSION/\./}-{extras,unit-parallel,integ-parallel,pandas-parallel,sso}-ci | sed 's/ /,/g'` - env: PYTHON_VERSION: ${{ matrix.python-version }} cloud_provider: ${{ matrix.cloud-provider }} @@ -232,10 +239,8 @@ jobs: # To specify the test name (in single test mode) pass this env variable: # SINGLE_TEST_NAME: test/path/filename.py::test_name shell: bash - - name: Combine coverages - run: python -m tox run -e coverage --skip-missing-interpreters false - shell: bash - uses: actions/upload-artifact@v4 + if: always() with: include-hidden-files: true name: coverage_${{ matrix.os.download_name }}-${{ matrix.python-version }}-${{ matrix.cloud-provider }} @@ -243,6 +248,7 @@ jobs: .tox/.coverage .tox/coverage.xml - uses: actions/upload-artifact@v4 + if: always() with: include-hidden-files: true name: junit_${{ matrix.os.download_name }}-${{ matrix.python-version }}-${{ matrix.cloud-provider }} @@ -370,6 +376,7 @@ jobs: TOX_PARALLEL_NO_SPINNER: 1 shell: bash - uses: actions/upload-artifact@v4 + if: always() with: include-hidden-files: true name: coverage_linux-fips-3.9-${{ matrix.cloud-provider }} @@ -377,6 +384,7 @@ jobs: .coverage coverage.xml - uses: actions/upload-artifact@v4 + if: always() with: include-hidden-files: true name: junit_linux-fips-3.9-${{ matrix.cloud-provider }} @@ -432,6 +440,7 @@ jobs: TOX_PARALLEL_NO_SPINNER: 1 shell: bash - uses: actions/upload-artifact@v4 + if: always() with: include-hidden-files: true name: coverage_linux-lambda-${{ matrix.python-version }}-${{ matrix.cloud-provider }} @@ -439,6 +448,7 @@ jobs: .coverage.py${{ env.shortver }}-lambda-ci junit.py${{ env.shortver }}-lambda-ci-dev.xml - uses: actions/upload-artifact@v4 + if: always() with: include-hidden-files: true name: junit_linux-lambda-${{ matrix.python-version }}-${{ matrix.cloud-provider }} @@ -555,7 +565,7 @@ jobs: shell: bash combine-coverage: - if: ${{ success() || failure() }} + if: always() name: Combine coverage needs: [lint, test, test-fips, test-lambda, test-aio] runs-on: ubuntu-latest @@ -587,6 +597,7 @@ jobs: dst_file = dst_dir / ".coverage.{}".format(src_file.parent.name[9:]) print("{} copy to {}".format(src_file, dst_file)) shutil.copy(str(src_file), str(dst_file))' + - name: Collect all JUnit XML files to one dir run: | python -c ' @@ -596,33 +607,43 @@ jobs: src_dir = Path("artifacts") dst_dir = Path(".") / "junit_results" dst_dir.mkdir() - # Collect all JUnit XML files with different naming patterns - for pattern in ["*/junit.*.xml", "*/junit.py*-lambda-ci-dev.xml"]: - for src_file in src_dir.glob(pattern): - dst_file = dst_dir / src_file.name - print("{} copy to {}".format(src_file, dst_file)) - shutil.copy(str(src_file), str(dst_file))' + for src_file in src_dir.glob("*/junit*.xml"): + artifact_name = src_file.parent.name + dst_file = dst_dir / f"{artifact_name}_{src_file.name}" + print("{} copy to {}".format(src_file, dst_file)) + shutil.copy(str(src_file), str(dst_file))' - name: Combine coverages - run: python -m tox run -e coverage + run: | + if [ -d ".tox" ] && [ "$(find .tox -name ".coverage*" -type f | wc -l)" -gt 0 ]; then + python -m tox run -e coverage + else + echo "No coverage files found, skipping coverage combination" + fi - name: Publish html coverage + if: ${{ success() }} uses: actions/upload-artifact@v4 with: include-hidden-files: true name: overall_cov_html path: .tox/htmlcov - name: Publish xml coverage + if: ${{ success() }} uses: actions/upload-artifact@v4 with: include-hidden-files: true name: overall_cov_xml path: .tox/coverage.xml - uses: codecov/codecov-action@v4 + if: ${{ success() }} with: files: .tox/coverage.xml token: ${{ secrets.CODECOV_TOKEN }} + name: coverage-${{ github.run_id }} + fail_ci_if_error: false + verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - files: junit_results/junit.*.xml + files: junit_results/*.xml diff --git a/tox.ini b/tox.ini index fa62839924..9ca4e77d96 100644 --- a/tox.ini +++ b/tox.ini @@ -188,7 +188,7 @@ depends = py39, py310, py311, py312, py313 [pytest] log_level = info addopts = -ra --strict-markers -junit_family = legacy +junit_family = xunit2 filterwarnings = error::UserWarning:cryptography.* error::cryptography.utils.CryptographyDeprecationWarning From c4233b1796211db7ff650714e48119818a55943f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Wed, 12 Nov 2025 21:43:47 +0100 Subject: [PATCH 12/15] [async] Applied #2569 to async code --- .github/workflows/build_test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 2adccdfb84..2d29cd2bbd 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -528,7 +528,9 @@ jobs: run: python -m tox run -e coverage --skip-missing-interpreters false shell: bash - uses: actions/upload-artifact@v4 + if: always() with: + include-hidden-files: true name: coverage_aio_${{ matrix.os.download_name }}-${{ matrix.python-version }}-${{ matrix.cloud-provider }} path: | .tox/.coverage From 62a635e72143ea8d7ca69feadcb39365ce3a85a6 Mon Sep 17 00:00:00 2001 From: Patryk Czajka Date: Wed, 8 Oct 2025 11:58:24 +0200 Subject: [PATCH 13/15] Update DESCRIPTION.md after 3.18.0 release (#2571) (cherry picked from commit 252a5f186f3c99aad105b44c5af509a43b7028d1) --- DESCRIPTION.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 5558302bbe..66eb8e9579 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -8,10 +8,10 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne # Release Notes - v4.1.0(TBD) - - Added `CERT_REVOCATION_CHECK_MODE` to `CLIENT_ENVIRONMENT` -- v4.0.0(October 01,2025) +- v4.0.0(October 09,2025) - Added support for checking certificates revocation using revocation lists (CRLs) + - Added `CERT_REVOCATION_CHECK_MODE` to `CLIENT_ENVIRONMENT` - Added the `workload_identity_impersonation_path` parameter to support service account impersonation for Workload Identity Federation on GCP and AWS workloads only - Fixed `get_results_from_sfqid` when using `DictCursor` and executing multiple statements at once - Added the `oauth_credentials_in_body` parameter supporting an option to send the oauth client credentials in the request body @@ -20,7 +20,6 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne - Revert changing exception type in case of token expired scenario for `Oauth` authenticator back to `DatabaseError` - Enhanced configuration file security checks with stricter permission validation. - Configuration files writable by group or others now raise a `ConfigSourceError` with detailed permission information, preventing potential credential tampering. - - Added support for pandas conversion for Day-time and Year-Month Interval types - Fixed the return type of `SnowflakeConnection.cursor(cursor_class)` to match the type of `cursor_class` - Constrained the types of `fetchone`, `fetchmany`, `fetchall` - As part of this fix, `DictCursor` is no longer a subclass of `SnowflakeCursor`; use `SnowflakeCursorBase` as a superclass of both. @@ -28,6 +27,9 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne - Add `ocsp_root_certs_dict_lock_timeout` connection parameter to set the timeout (in seconds) for acquiring the lock on the OCSP root certs dictionary. Default value for this parameter is -1 which indicates no timeout. - Fixed behaviour of trying S3 Transfer Accelerate endpoint by default for internal stages, and always getting HTTP403 due to permissions missing on purpose. Now /accelerate is not attempted. +- v3.18.0(October 03,2025) + - Added support for pandas conversion for Day-time and Year-Month Interval types + - v3.17.4(September 22,2025) - Added support for intermediate certificates as roots when they are stored in the trust store - Bumped up vendored `urllib3` to `2.5.0` and `requests` to `v2.32.5` From 671cd4f6c3531594b9ae70af3f13d58e260479e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Wed, 19 Nov 2025 12:31:51 +0100 Subject: [PATCH 14/15] [async] review changes --- src/snowflake/connector/aio/_session_manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/snowflake/connector/aio/_session_manager.py b/src/snowflake/connector/aio/_session_manager.py index 09d6155dfc..f6b80d2f1b 100644 --- a/src/snowflake/connector/aio/_session_manager.py +++ b/src/snowflake/connector/aio/_session_manager.py @@ -11,7 +11,10 @@ from .. import OperationalError from ..errorcode import ER_OCSP_RESPONSE_CERT_STATUS_REVOKED -from ..ssl_wrap_socket import FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME, FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT +from ..ssl_wrap_socket import ( + FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME, + FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT, +) from ._ocsp_asn1crypto import SnowflakeOCSPAsn1Crypto if TYPE_CHECKING: From da750c9ae1a2d1213327080eff6c1cf091ada885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Paw=C5=82owski?= Date: Fri, 21 Nov 2025 13:03:17 +0100 Subject: [PATCH 15/15] [async] commented parts with ocsp revoked certs timeout in tests broken (reapply #2559) --- .../connector/aio/_session_manager.py | 8 +- test/integ/aio_it/test_connection_async.py | 173 +++++++++--------- 2 files changed, 90 insertions(+), 91 deletions(-) diff --git a/src/snowflake/connector/aio/_session_manager.py b/src/snowflake/connector/aio/_session_manager.py index f6b80d2f1b..1e3ed27549 100644 --- a/src/snowflake/connector/aio/_session_manager.py +++ b/src/snowflake/connector/aio/_session_manager.py @@ -11,10 +11,7 @@ from .. import OperationalError from ..errorcode import ER_OCSP_RESPONSE_CERT_STATUS_REVOKED -from ..ssl_wrap_socket import ( - FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME, - FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT, -) +from ..ssl_wrap_socket import FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME from ._ocsp_asn1crypto import SnowflakeOCSPAsn1Crypto if TYPE_CHECKING: @@ -105,7 +102,8 @@ async def validate_ocsp( ocsp_response_cache_uri=FEATURE_OCSP_RESPONSE_CACHE_FILE_NAME, use_fail_open=self._snowflake_ocsp_mode == OCSPMode.FAIL_OPEN, hostname=hostname, - root_certs_dict_lock_timeout=FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT, + # TODO: uncomment when issues with ocsp revoked certs in tests are fixed (reapply #2559) + # root_certs_dict_lock_timeout=FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT, ).validate(hostname, protocol, session_manager=session_manager) if not v: raise OperationalError( diff --git a/test/integ/aio_it/test_connection_async.py b/test/integ/aio_it/test_connection_async.py index ff7bd699ec..3d9105ae2f 100644 --- a/test/integ/aio_it/test_connection_async.py +++ b/test/integ/aio_it/test_connection_async.py @@ -1335,92 +1335,93 @@ async def test_ocsp_mode_insecure_mode_and_disable_ocsp_checks_mismatch_ocsp_ena assert "snowflake.connector.aio._ocsp_snowflake" not in caplog.text -@pytest.mark.skipolddriver -async def test_root_certs_dict_lock_timeout_fail_open(conn_cnx): - """Test OCSP root certificates lock timeout with fail-open mode and side effect mock.""" - - override_config = { - "ocsp_fail_open": True, - "ocsp_root_certs_dict_lock_timeout": 0.1, - } - - with mock.patch( - "snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" - ) as mock_lock: - snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = ( - {} - ) - - mock_lock.acquire = mock.MagicMock(return_value=False) - mock_lock.release = mock.MagicMock() - - async with conn_cnx(**override_config) as conn: - try: - async with conn.cursor() as cur: - assert await (await cur.execute("select 1")).fetchall() == [(1,)] - - if mock_lock.acquire.called: - mock_lock.acquire.assert_called_with(timeout=0.1) - assert conn._ocsp_root_certs_dict_lock_timeout == 0.1 - finally: - await conn.close() - - -@pytest.mark.skipolddriver -@pytest.mark.parametrize( - "ocsp_fail_open,timeout_value,expected_timeout", - [ - (False, 1, 1), # fail-close mode with 1 second timeout - (True, 2, 2), # fail-open mode with 2 second timeout - ], -) -async def test_root_certs_dict_lock_timeout_with_property_mock( - conn_cnx, ocsp_fail_open, timeout_value, expected_timeout -): - """Test OCSP root certificates lock timeout with property mock for different configurations.""" - config = { - "ocsp_fail_open": ocsp_fail_open, - "ocsp_root_certs_dict_lock_timeout": timeout_value, - } - - with mock.patch( - "snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" - ) as mock_lock: - snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = ( - {} - ) - - type(mock_lock).acquire = mock.PropertyMock(return_value=lambda timeout: False) - type(mock_lock).release = mock.PropertyMock(return_value=lambda: None) - - async with conn_cnx(**config) as conn: - async with conn.cursor() as cur: - assert await (await cur.execute("select 1")).fetchall() == [(1,)] - - assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout - await conn.close() - - -@pytest.mark.skipolddriver -@pytest.mark.parametrize( - "config,expected_timeout", - [ - ({"ocsp_fail_open": True, "ocsp_root_certs_dict_lock_timeout": 0.001}, 0.001), - ({"ocsp_fail_open": True}, -1), # no timeout specified, should default to -1 - ], -) -async def test_root_certs_dict_lock_timeout_basic_config( - conn_cnx, config, expected_timeout -): - """Test OCSP root certificates lock timeout basic configuration without mocking.""" - async with conn_cnx(**config) as conn: - try: - async with conn.cursor() as cur: - assert await (await cur.execute("select 1")).fetchall() == [(1,)] - - assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout - finally: - await conn.close() +# TODO: uncomment when issues with ocsp revoked certs in tests are fixed (reapply #2559) +# @pytest.mark.skipolddriver +# async def test_root_certs_dict_lock_timeout_fail_open(conn_cnx): +# """Test OCSP root certificates lock timeout with fail-open mode and side effect mock.""" +# +# override_config = { +# "ocsp_fail_open": True, +# "ocsp_root_certs_dict_lock_timeout": 0.1, +# } +# +# with mock.patch( +# "snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" +# ) as mock_lock: +# snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = ( +# {} +# ) +# +# mock_lock.acquire = mock.MagicMock(return_value=False) +# mock_lock.release = mock.MagicMock() +# +# async with conn_cnx(**override_config) as conn: +# try: +# async with conn.cursor() as cur: +# assert await (await cur.execute("select 1")).fetchall() == [(1,)] +# +# if mock_lock.acquire.called: +# mock_lock.acquire.assert_called_with(timeout=0.1) +# assert conn._ocsp_root_certs_dict_lock_timeout == 0.1 +# finally: +# await conn.close() +# +# +# @pytest.mark.skipolddriver +# @pytest.mark.parametrize( +# "ocsp_fail_open,timeout_value,expected_timeout", +# [ +# (False, 1, 1), # fail-close mode with 1 second timeout +# (True, 2, 2), # fail-open mode with 2 second timeout +# ], +# ) +# async def test_root_certs_dict_lock_timeout_with_property_mock( +# conn_cnx, ocsp_fail_open, timeout_value, expected_timeout +# ): +# """Test OCSP root certificates lock timeout with property mock for different configurations.""" +# config = { +# "ocsp_fail_open": ocsp_fail_open, +# "ocsp_root_certs_dict_lock_timeout": timeout_value, +# } +# +# with mock.patch( +# "snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT_LOCK" +# ) as mock_lock: +# snowflake.connector.aio._ocsp_snowflake.SnowflakeOCSP.ROOT_CERTIFICATES_DICT = ( +# {} +# ) +# +# type(mock_lock).acquire = mock.PropertyMock(return_value=lambda timeout: False) +# type(mock_lock).release = mock.PropertyMock(return_value=lambda: None) +# +# async with conn_cnx(**config) as conn: +# async with conn.cursor() as cur: +# assert await (await cur.execute("select 1")).fetchall() == [(1,)] +# +# assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout +# await conn.close() +# +# +# @pytest.mark.skipolddriver +# @pytest.mark.parametrize( +# "config,expected_timeout", +# [ +# ({"ocsp_fail_open": True, "ocsp_root_certs_dict_lock_timeout": 0.001}, 0.001), +# ({"ocsp_fail_open": True}, -1), # no timeout specified, should default to -1 +# ], +# ) +# async def test_root_certs_dict_lock_timeout_basic_config( +# conn_cnx, config, expected_timeout +# ): +# """Test OCSP root certificates lock timeout basic configuration without mocking.""" +# async with conn_cnx(**config) as conn: +# try: +# async with conn.cursor() as cur: +# assert await (await cur.execute("select 1")).fetchall() == [(1,)] +# +# assert conn._ocsp_root_certs_dict_lock_timeout == expected_timeout +# finally: +# await conn.close() @pytest.mark.skipolddriver