Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
# Release Notes
- v3.16.1(TBD)
- Added in-band OCSP exception telemetry.
- 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.

- v3.16.0(July 04,2025)
- Bumped numpy dependency from <2.1.0 to <=2.2.4.
Expand Down
5 changes: 5 additions & 0 deletions src/snowflake/connector/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ def _get_private_bytes_from_file(
True,
bool,
), # SNOW-XXXXX: remove the check_arrow_conversion_error_on_every_column flag
"ocsp_root_certs_dict_lock_timeout": (
-1,
int,
),
"external_session_id": (
None,
str,
Expand Down Expand Up @@ -460,6 +464,7 @@ class SnowflakeConnection:
token_file_path: The file path of the token file. If both token and token_file_path are provided, the token in token_file_path will be used.
unsafe_file_write: When true, files downloaded by GET will be saved with 644 permissions. Otherwise, files will be saved with safe - owner-only permissions: 600.
check_arrow_conversion_error_on_every_column: When true, the error check after the conversion from arrow to python types will happen for every column in the row. This is a new behaviour which fixes the bug that caused the type errors to trigger silently when occurring at any place other than last column in a row. To revert the previous (faulty) behaviour, please set this flag to false.
ocsp_root_certs_dict_lock_timeout: Timeout for the OCSP root certs dict lock in seconds. Default value is -1, which means no timeout.
"""

OCSP_ENV_LOCK = Lock()
Expand Down
6 changes: 6 additions & 0 deletions src/snowflake/connector/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,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 -1
)

# This is to address the issue where requests hangs
_ = "dummy".encode("idna").decode("utf-8")
Expand Down
130 changes: 72 additions & 58 deletions src/snowflake/connector/ocsp_snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,7 @@ def __init__(
use_ocsp_cache_server=None,
use_post_method: bool = True,
use_fail_open: bool = True,
root_certs_dict_lock_timeout: int = -1,
**kwargs,
) -> None:
self.test_mode = os.getenv("SF_OCSP_TEST_MODE", None)
Expand All @@ -1040,6 +1041,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)
Expand Down Expand Up @@ -1410,67 +1412,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:
Expand Down
2 changes: 2 additions & 0 deletions src/snowflake/connector/ssl_wrap_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

DEFAULT_OCSP_MODE: OCSPMode = OCSPMode.FAIL_OPEN
FEATURE_OCSP_MODE: OCSPMode = DEFAULT_OCSP_MODE
FEATURE_ROOT_CERTS_DICT_LOCK_TIMEOUT: int = -1

"""
OCSP Response cache file name
Expand Down Expand Up @@ -84,6 +85,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(
Expand Down
161 changes: 161 additions & 0 deletions test/integ/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import stat
import tempfile
import threading
import time
import warnings
import weakref
from unittest import mock
from unittest.mock import MagicMock, PropertyMock, patch
from uuid import uuid4

import pytest
Expand Down Expand Up @@ -1487,6 +1489,165 @@ 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(db_parameters, caplog):
def mock_acquire_times_out(timeout=None):
"""Mock acquire method that always times out after the specified timeout."""
if timeout is not None and timeout > 0:
time.sleep(timeout)
return False

config = {
"user": db_parameters["user"],
"password": db_parameters["password"],
"host": db_parameters["host"],
"port": db_parameters["port"],
"account": db_parameters["account"],
"schema": db_parameters["schema"],
"database": db_parameters["database"],
"protocol": db_parameters["protocol"],
"timezone": "UTC",
"ocsp_fail_open": True,
"ocsp_root_certs_dict_lock_timeout": 0.1,
}

caplog.set_level(logging.INFO, "snowflake.connector.ocsp_snowflake")

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(side_effect=mock_acquire_times_out)
mock_lock.release = MagicMock()

conn = snowflake.connector.connect(**config)

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
def test_root_certs_dict_lock_timeout(db_parameters, caplog):
config_fail_close = {
"user": db_parameters["user"],
"password": db_parameters["password"],
"host": db_parameters["host"],
"port": db_parameters["port"],
"account": db_parameters["account"],
"schema": db_parameters["schema"],
"database": db_parameters["database"],
"protocol": db_parameters["protocol"],
"timezone": "UTC",
"ocsp_fail_open": False,
"ocsp_root_certs_dict_lock_timeout": 1,
}

caplog.set_level(logging.INFO, "snowflake.connector.ocsp_snowflake")

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)

conn = snowflake.connector.connect(**config_fail_close)
with conn.cursor() as cur:
assert cur.execute("select 1").fetchall() == [(1,)]

assert conn._ocsp_root_certs_dict_lock_timeout == 1
conn.close()

caplog.clear()

config_fail_open = {
"user": db_parameters["user"],
"password": db_parameters["password"],
"host": db_parameters["host"],
"port": db_parameters["port"],
"account": db_parameters["account"],
"schema": db_parameters["schema"],
"database": db_parameters["database"],
"protocol": db_parameters["protocol"],
"timezone": "UTC",
"ocsp_fail_open": True, # fail-open mode
"ocsp_root_certs_dict_lock_timeout": 2, # 2 second timeout
}

caplog.set_level(logging.INFO, "snowflake.connector.ocsp_snowflake")

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)

conn = snowflake.connector.connect(**config_fail_open)
with conn.cursor() as cur:
assert cur.execute("select 1").fetchall() == [(1,)]

assert conn._ocsp_root_certs_dict_lock_timeout == 2
conn.close()

caplog.clear()

config_short_timeout = {
"user": db_parameters["user"],
"password": db_parameters["password"],
"host": db_parameters["host"],
"port": db_parameters["port"],
"account": db_parameters["account"],
"schema": db_parameters["schema"],
"database": db_parameters["database"],
"protocol": db_parameters["protocol"],
"timezone": "UTC",
"ocsp_fail_open": True,
"ocsp_root_certs_dict_lock_timeout": 0.001,
}

conn = snowflake.connector.connect(**config_short_timeout)
try:
with conn.cursor() as cur:
assert cur.execute("select 1").fetchall() == [(1,)]

assert conn._ocsp_root_certs_dict_lock_timeout == 0.001
finally:
conn.close()

config_no_timeout = {
"user": db_parameters["user"],
"password": db_parameters["password"],
"host": db_parameters["host"],
"port": db_parameters["port"],
"account": db_parameters["account"],
"schema": db_parameters["schema"],
"database": db_parameters["database"],
"protocol": db_parameters["protocol"],
"timezone": "UTC",
"ocsp_fail_open": True,
}

conn = snowflake.connector.connect(**config_no_timeout)
try:
with conn.cursor() as cur:
assert cur.execute("select 1").fetchall() == [(1,)]

assert conn._ocsp_root_certs_dict_lock_timeout == -1
finally:
conn.close()


@pytest.mark.skipolddriver
def test_ocsp_mode_insecure_mode_deprecation_warning(conn_cnx):
with warnings.catch_warnings(record=True) as w:
Expand Down
Loading
Loading