Skip to content

Commit 045b634

Browse files
Handle an empty access_token the same way on every auth path
- Keep certificate authentication working when an empty token is passed: the empty-token rejection now comes after the cert-auth branch, where the default OAuth login would otherwise start. - Reject an empty token on the Reyden pre-check path too, which skips Thrift and previously upgraded the connection to databricks-oauth. - Remove the duplicate RequestError import in the test. - Add changelog entries. Signed-off-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
1 parent 5262855 commit 045b634

6 files changed

Lines changed: 46 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release History
22

3+
# Unreleased
4+
- Fix: transport failures (connection refused, proxy/tunnel errors) on the Thrift backend raise `RequestError` (`OperationalError`) instead of a raw `urllib3` exception.
5+
- Fix: an explicitly empty `access_token` raises `No valid authentication settings! access_token is empty` instead of silently starting the interactive browser OAuth login, including when a known Reyden warehouse skips Thrift. Explicit `auth_type`, `credentials_provider` and certificate authentication still take precedence.
6+
37
# 4.6.0 (2026-09-24)
48
- Upgrade Databricks SQL Kernel to 1.1.0; the kernel dependency is now stable and no longer experimental.
59
- Transparently auto-recover Thrift connections to Reyden / Real-Time warehouses: when a warehouse rejects the default Thrift protocol (SQLSTATE `KP001`), the session is re-opened on the kernel backend and the warehouse is remembered so later connections skip Thrift. Applies only when no backend was chosen explicitly.

‎src/databricks/sql/auth/auth.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from databricks.sql.auth.common import AuthType, ClientContext
1111
from databricks.sql.auth.token_federation import TokenFederationProvider
1212

13+
EMPTY_ACCESS_TOKEN_MESSAGE = "No valid authentication settings! access_token is empty"
14+
1315

1416
def get_auth_provider(cfg: ClientContext, http_client):
1517
# Determine the base auth provider
@@ -42,15 +44,15 @@ def get_auth_provider(cfg: ClientContext, http_client):
4244
http_client,
4345
cfg.auth_type,
4446
)
45-
elif cfg.access_token is not None:
46-
if not cfg.access_token:
47-
# An explicitly empty token is a missing credential; never fall
48-
# back to an interactive browser login for it.
49-
raise RuntimeError("No valid authentication settings! access_token is empty")
47+
elif cfg.access_token:
5048
base_provider = AccessTokenAuthProvider(cfg.access_token)
5149
elif cfg.use_cert_as_auth and cfg.tls_client_cert_file:
5250
# no op authenticator. authentication is performed using ssl certificate outside of headers
5351
base_provider = AuthProvider()
52+
elif cfg.access_token is not None:
53+
# An explicitly empty token is a missing credential; never fall back
54+
# to the default interactive browser login for it.
55+
raise RuntimeError(EMPTY_ACCESS_TOKEN_MESSAGE)
5456
else:
5557
if (
5658
cfg.oauth_redirect_port_range is not None

‎src/databricks/sql/client.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@
6767

6868
from databricks.sql.result_set import ResultSet
6969
from databricks.sql.types import Row, SSLOptions
70-
from databricks.sql.auth.auth import get_python_sql_connector_auth_provider
70+
from databricks.sql.auth.auth import (
71+
EMPTY_ACCESS_TOKEN_MESSAGE,
72+
get_python_sql_connector_auth_provider,
73+
)
7174
from databricks.sql.experimental.oauth_persistence import OAuthPersistence
7275
from databricks.sql.session import Session
7376
from databricks.sql.backend.types import CommandId, BackendType, CommandState, SessionId
@@ -581,6 +584,11 @@ def kernel_recovery_kwargs() -> dict:
581584
or recovery_kwargs.get("credentials_provider")
582585
)
583586
if recovery_kwargs.get("auth_type") is None and not has_credential_shape:
587+
if recovery_kwargs.get("access_token") is not None:
588+
# An explicitly empty token: the Thrift path rejects it
589+
# rather than defaulting to the interactive OAuth login
590+
# (see get_auth_provider), so do the same here.
591+
raise RuntimeError(EMPTY_ACCESS_TOKEN_MESSAGE)
584592
recovery_kwargs["auth_type"] = AuthType.DATABRICKS_OAUTH.value
585593
return recovery_kwargs
586594

‎tests/unit/test_auth.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ def test_get_python_sql_connector_auth_provider_empty_access_token(self):
159159
"moderakh-test.cloud.databricks.com", MagicMock(), access_token=""
160160
)
161161

162+
def test_get_python_sql_connector_auth_provider_empty_access_token_cert_auth(self):
163+
"""An empty token does not override certificate authentication."""
164+
auth_provider = get_python_sql_connector_auth_provider(
165+
"moderakh-test.cloud.databricks.com",
166+
MagicMock(),
167+
access_token="",
168+
_tls_client_cert_file="fake.cert",
169+
_use_cert_as_auth="abc",
170+
)
171+
self.assertIsNotNone(auth_provider)
172+
162173
def test_get_python_sql_connector_auth_provider_external(self):
163174
class MyProvider(CredentialsProvider):
164175
def auth_type(self) -> str:

‎tests/unit/test_session.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,20 @@ def test_oauth_default_recovery_injects_databricks_oauth_auth_type(
954954
finally:
955955
conn.close()
956956

957+
@patch("%s.session.ThriftDatabricksClient" % PACKAGE)
958+
def test_known_reyden_empty_access_token_is_rejected(self, mock_thrift):
959+
# The Thrift path rejects an explicitly empty token instead of starting
960+
# the interactive OAuth login; skipping Thrift for a known Reyden
961+
# warehouse must not turn it into a databricks-oauth login either.
962+
from databricks.sql.backend import reyden_warehouse_cache
963+
964+
reyden_warehouse_cache.mark_reyden(self.HOST, "wh-reyden")
965+
with self._fake_kernel() as mock_kernel:
966+
with pytest.raises(RuntimeError, match="access_token is empty"):
967+
self._connect(access_token="")
968+
mock_kernel.assert_not_called()
969+
mock_thrift.return_value.open_session.assert_not_called()
970+
957971
@patch("%s.session.ThriftDatabricksClient" % PACKAGE)
958972
def test_pat_recovery_does_not_inject_auth_type(self, mock_thrift):
959973
# With a credential shape present (here a PAT) the kernel routes on it

‎tests/unit/test_thrift_backend.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1927,7 +1927,7 @@ def test_make_request_will_retry_GetOperationStatus(
19271927

19281928
import thrift, errno
19291929
from databricks.sql.thrift_api.TCLIService.TCLIService import Client
1930-
from databricks.sql.exc import RequestError, RequestError
1930+
from databricks.sql.exc import RequestError
19311931
from databricks.sql.utils import NoRetryReason
19321932

19331933
this_gos_name = "GetOperationStatus"

0 commit comments

Comments
 (0)