Skip to content

Commit 14651e9

Browse files
Add kerberos auth support
Signed-off-by: Vikrant Puppala <[email protected]>
1 parent 4d54557 commit 14651e9

File tree

9 files changed

+473
-16
lines changed

9 files changed

+473
-16
lines changed

poetry.lock

Lines changed: 411 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pyarrow = [
2626
{ version = ">=18.0.0", python = ">=3.13", optional=true }
2727
]
2828
pyjwt = "^2.0.0"
29+
requests-kerberos = "^0.15.0"
2930

3031

3132
[tool.poetry.extras]

src/databricks/sql/auth/common.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ def __init__(
4646
retry_stop_after_attempts_duration: Optional[float] = None,
4747
retry_delay_default: Optional[float] = None,
4848
retry_dangerous_codes: Optional[List[int]] = None,
49-
http_proxy: Optional[str] = None,
50-
proxy_username: Optional[str] = None,
51-
proxy_password: Optional[str] = None,
49+
proxy_auth_method: Optional[str] = None,
5250
pool_connections: Optional[int] = None,
5351
pool_maxsize: Optional[int] = None,
5452
user_agent: Optional[str] = None,
@@ -79,9 +77,7 @@ def __init__(
7977
)
8078
self.retry_delay_default = retry_delay_default or 5.0
8179
self.retry_dangerous_codes = retry_dangerous_codes or []
82-
self.http_proxy = http_proxy
83-
self.proxy_username = proxy_username
84-
self.proxy_password = proxy_password
80+
self.proxy_auth_method = proxy_auth_method
8581
self.pool_connections = pool_connections or 10
8682
self.pool_maxsize = pool_maxsize or 20
8783
self.user_agent = user_agent

src/databricks/sql/auth/thrift_http_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(
3232
ssl_options: Optional[SSLOptions] = None,
3333
max_connections: int = 1,
3434
retry_policy: Union[DatabricksRetryPolicy, int] = 0,
35+
**kwargs,
3536
):
3637
self._ssl_options = ssl_options
3738

@@ -63,7 +64,10 @@ def __init__(
6364
self.path += "?%s" % parsed.query
6465

6566
# Handle proxy settings using shared utility
66-
proxy_uri, proxy_auth = detect_and_parse_proxy(self.scheme, self.host)
67+
proxy_auth_method = kwargs.get("_proxy_auth_method")
68+
proxy_uri, proxy_auth = detect_and_parse_proxy(
69+
self.scheme, self.host, proxy_auth_method=proxy_auth_method
70+
)
6771

6872
if proxy_uri:
6973
parsed_proxy = urllib.parse.urlparse(proxy_uri)

src/databricks/sql/backend/sea/utils/http_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ def __init__(
125125
self.retry_policy = 0
126126

127127
# Handle proxy settings using shared utility
128-
proxy_uri, proxy_auth = detect_and_parse_proxy(self.scheme, self.host)
128+
proxy_auth_method = kwargs.get("_proxy_auth_method")
129+
proxy_uri, proxy_auth = detect_and_parse_proxy(
130+
self.scheme, self.host, proxy_auth_method=proxy_auth_method
131+
)
129132

130133
if proxy_uri:
131134
parsed_proxy = urllib.parse.urlparse(proxy_uri)

src/databricks/sql/backend/thrift_backend.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ def __init__(
191191
self.force_dangerous_codes = kwargs.get("_retry_dangerous_codes", [])
192192

193193
additional_transport_args = {}
194+
195+
# Add proxy authentication method if specified
196+
proxy_auth_method = kwargs.get("_proxy_auth_method")
197+
if proxy_auth_method:
198+
additional_transport_args["_proxy_auth_method"] = proxy_auth_method
199+
194200
_max_redirects: Union[None, int] = kwargs.get("_retry_max_redirects")
195201

196202
if _max_redirects:

src/databricks/sql/common/http_utils.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ssl
22
import urllib.parse
33
import urllib.request
4+
import logging
45
from typing import Dict, Any, Optional, Tuple, Union
56

67
from urllib3 import HTTPConnectionPool, HTTPSConnectionPool, ProxyManager
@@ -9,9 +10,14 @@
910
from databricks.sql.auth.retry import DatabricksRetryPolicy
1011
from databricks.sql.types import SSLOptions
1112

13+
logger = logging.getLogger(__name__)
14+
1215

1316
def detect_and_parse_proxy(
14-
scheme: str, host: str = None, skip_bypass: bool = False
17+
scheme: str,
18+
host: str = None,
19+
skip_bypass: bool = False,
20+
proxy_auth_method: Optional[str] = None,
1521
) -> Tuple[Optional[str], Optional[Dict[str, str]]]:
1622
"""
1723
Detect system proxy and return proxy URI and headers using standardized logic.
@@ -20,6 +26,7 @@ def detect_and_parse_proxy(
2026
scheme: URL scheme (http/https)
2127
host: Target hostname (optional, only needed for bypass checking)
2228
skip_bypass: If True, skip proxy bypass checking and return proxy config if found
29+
proxy_auth_method: Authentication method ('basic', 'negotiate', or None)
2330
2431
Returns:
2532
Tuple of (proxy_uri, proxy_headers) or (None, None) if no proxy
@@ -40,10 +47,41 @@ def detect_and_parse_proxy(
4047
return None, None
4148

4249
parsed_proxy = urllib.parse.urlparse(proxy)
43-
proxy_headers = create_basic_proxy_auth_headers(parsed_proxy)
50+
51+
# Generate appropriate auth headers based on method
52+
if proxy_auth_method == "negotiate":
53+
proxy_headers = _generate_negotiate_headers(parsed_proxy.hostname)
54+
elif proxy_auth_method == "basic" or proxy_auth_method is None:
55+
# Default to basic if method not specified (backward compatibility)
56+
proxy_headers = create_basic_proxy_auth_headers(parsed_proxy)
57+
else:
58+
raise ValueError(f"Unsupported proxy_auth_method: {proxy_auth_method}")
59+
4460
return proxy, proxy_headers
4561

4662

63+
def _generate_negotiate_headers(proxy_hostname: str) -> Optional[Dict[str, str]]:
64+
"""Generate Kerberos/SPNEGO authentication headers"""
65+
try:
66+
from requests_kerberos import HTTPKerberosAuth
67+
68+
logger.debug(
69+
f"Attempting to generate Kerberos SPNEGO token for proxy: {proxy_hostname}"
70+
)
71+
auth = HTTPKerberosAuth()
72+
negotiate_details = auth.generate_request_header(
73+
None, proxy_hostname, is_preemptive=True
74+
)
75+
if negotiate_details:
76+
return {"proxy-authorization": negotiate_details}
77+
else:
78+
logger.debug("Unable to generate kerberos proxy auth headers")
79+
except Exception as e:
80+
logger.error(f"Error generating Kerberos proxy auth headers: {e}")
81+
82+
return None
83+
84+
4785
def create_basic_proxy_auth_headers(parsed_proxy) -> Optional[Dict[str, str]]:
4886
"""
4987
Create basic auth headers for proxy if credentials are provided.

src/databricks/sql/common/unified_http_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from databricks.sql.common.http import HttpMethod
1616
from databricks.sql.common.http_utils import (
1717
detect_and_parse_proxy,
18-
create_basic_proxy_auth_headers,
1918
)
2019

2120
logger = logging.getLogger(__name__)
@@ -124,7 +123,9 @@ def _setup_pool_managers(self):
124123
try:
125124
# Use shared proxy detection logic, skipping bypass since we handle that per-request
126125
proxy_url, proxy_auth = detect_and_parse_proxy(
127-
self.scheme, skip_bypass=True
126+
self.scheme,
127+
skip_bypass=True,
128+
proxy_auth_method=self.config.proxy_auth_method,
128129
)
129130

130131
if proxy_url:

src/databricks/sql/utils.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -919,9 +919,7 @@ def build_client_context(server_hostname: str, version: str, **kwargs):
919919
),
920920
retry_delay_default=kwargs.get("_retry_delay_default"),
921921
retry_dangerous_codes=kwargs.get("_retry_dangerous_codes"),
922-
http_proxy=kwargs.get("_http_proxy"),
923-
proxy_username=kwargs.get("_proxy_username"),
924-
proxy_password=kwargs.get("_proxy_password"),
922+
proxy_auth_method=kwargs.get("_proxy_auth_method"),
925923
pool_connections=kwargs.get("_pool_connections"),
926924
pool_maxsize=kwargs.get("_pool_maxsize"),
927925
)

0 commit comments

Comments
 (0)