Skip to content

Commit 8f2a73a

Browse files
feat(kernel): thread Azure Entra OAuth (U2M + SP M2M) through the auth bridge
The kernel auth bridge rejected azure-oauth and had no azure-sp-m2m path. Route both Azure auth types onto the kernel's generic OAuth flows (the kernel needs no Azure-specific code; PR databricks/databricks-sql-kernel#263 added the token_url/scope override plumbing this relies on): - azure-oauth (Azure AD U2M) -> oauth-u2m with the Azure app client id (96eecda7-...), redirect port 8030, and the {app_id}/user_impersonation offline_access delegated scope (via AzureOAuthEndpointCollection, honoring DATABRICKS_AZURE_TENANT_ID). The kernel discovers endpoints via the workspace /oidc redirector. (PECOBLR-4120) - azure-sp-m2m (Azure service principal) -> oauth-m2m with the Azure creds, an Entra v2.0 token_url, and the {effective_app_id}/.default scope. Requires an explicit azure_tenant_id (the kernel path does not auto-discover it). The management-token header / azure_workspace_resource_id are not applied on the kernel path -- no SQL connector uses them, matching Go and Node. (PECOBLR-4141) kernel_auth_kwargs now takes hostname (for the effective Azure app id); the client passes self._server_hostname. TDD: replaced the azure-oauth NotSupportedError test with routing tests and added a TestKernelAzureSpM2M suite (routing, required tenant/creds, federation client id). 50 bridge tests pass; black clean. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
1 parent 0a8f1d2 commit 8f2a73a

4 files changed

Lines changed: 211 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Release History
22

33
# Unreleased
4-
- Kernel backend (`use_kernel=True`): OAuth U2M with `auth_type="databricks-oauth"` now forwards the connector's `databricks-sql-python` OAuth-app bundle (`client_id` + `sql offline_access` scopes + redirect port) into the kernel, so a bare U2M connection authenticates as `databricks-sql-python` — parity with the Thrift path — instead of inheriting the kernel's own `databricks-sql-connector` default. A caller-supplied `oauth_client_id` (with its coupled `oauth_redirect_port`) is honored, as is a caller-supplied `oauth_scopes`; absent one, the connector default (`sql offline_access`) is forwarded. Note: the kernel binds a single U2M redirect port, so unlike the Thrift path (which tries the full `8020..8024` range) the kernel path uses only one port and does not fall back to the next port if it is already bound — pass `oauth_redirect_port` (with `oauth_client_id`) to pick a free one on a port collision. `auth_type="azure-oauth"` (Azure AD) is not yet supported on the kernel path and raises `NotSupportedError` — use the Thrift backend for it (PECOBLR-4040; Azure tracked by PECOBLR-4120)
4+
- Kernel backend (`use_kernel=True`): OAuth U2M with `auth_type="databricks-oauth"` now forwards the connector's `databricks-sql-python` OAuth-app bundle (`client_id` + `sql offline_access` scopes + redirect port) into the kernel, so a bare U2M connection authenticates as `databricks-sql-python` — parity with the Thrift path — instead of inheriting the kernel's own `databricks-sql-connector` default. A caller-supplied `oauth_client_id` (with its coupled `oauth_redirect_port`) is honored, as is a caller-supplied `oauth_scopes`; absent one, the connector default (`sql offline_access`) is forwarded. Note: the kernel binds a single U2M redirect port, so unlike the Thrift path (which tries the full `8020..8024` range) the kernel path uses only one port and does not fall back to the next port if it is already bound — pass `oauth_redirect_port` (with `oauth_client_id`) to pick a free one on a port collision (PECOBLR-4040)
5+
- Kernel backend (`use_kernel=True`): **Azure Entra (Azure AD) OAuth is now supported.** Both connector Azure auth types route onto the kernel's generic OAuth flows: `auth_type="azure-oauth"` (Azure AD U2M) forwards the Azure app bundle (client id `96eecda7-…`, redirect port `8030`, and the `{app_id}/user_impersonation offline_access` delegated scope, honoring `DATABRICKS_AZURE_TENANT_ID`) to the kernel's U2M browser flow; `auth_type="azure-sp-m2m"` (Azure service principal) forwards the Azure credentials with an Entra v2.0 token endpoint and the `{app_id}/.default` scope to the kernel's M2M flow. `azure-sp-m2m` requires an explicit `azure_tenant_id` on the kernel path (it does not auto-discover the tenant as the Thrift path does), and the Azure management-token header / `azure_workspace_resource_id` are not applied — the Databricks-audience token authenticates service principals that are workspace principals, matching the Go and Node SQL connectors (PECOBLR-4141; PECOBLR-4120)
56

67
# 4.4.0 (2026-07-22)
78
- Raised the minimum supported Python version to 3.10, dropping the end-of-life 3.8/3.9, to update the lockfile and clear CVE-flagged dependencies in the repo (databricks/databricks-sql-python#798)

src/databricks/sql/backend/kernel/auth_bridge.py

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,24 @@
2020
app bundle (``client_id`` + ``redirect_ports`` list, with the optional
2121
``oauth_client_id`` / ``oauth_redirect_port`` overriding it) is
2222
forwarded to the kernel's ``auth_type='oauth-u2m'`` and the kernel
23-
runs the browser flow itself. ``azure-oauth`` (Azure AD) is **not yet
24-
supported** on the kernel path and is rejected with
25-
``NotSupportedError`` — the kernel resolves OAuth endpoints only from
26-
the workspace-native OIDC config and cannot drive the Azure AD flow
27-
(PECOBLR-4120).
23+
runs the browser flow itself.
24+
- **Azure Entra (Azure AD)** — both Azure auth types route to the
25+
kernel's *generic* OAuth flows with Azure values as overrides (the
26+
kernel needs no Azure-specific code):
27+
28+
- ``azure-oauth`` (U2M) → ``oauth-u2m`` with the Azure app client id
29+
(``96eecda7-…``), redirect port ``8030``, and the AAD delegated scope
30+
``{app_id}/user_impersonation offline_access`` (via
31+
``AzureOAuthEndpointCollection``, honoring ``DATABRICKS_AZURE_TENANT_ID``).
32+
The kernel discovers endpoints via the workspace ``/oidc`` redirector,
33+
which an Azure workspace redirects to Entra (PECOBLR-4120).
34+
- ``azure-sp-m2m`` (M2M) → ``oauth-m2m`` with the Azure service-principal
35+
credentials, an Entra v2.0 ``token_url``, and the
36+
``{effective_app_id}/.default`` scope (PECOBLR-4141). ``azure_tenant_id``
37+
is required (the kernel path does not auto-discover it). The Azure
38+
management-token header and ``azure_workspace_resource_id`` are **not**
39+
applied on the kernel path — no SQL connector uses them; an SP that is
40+
not a workspace member (RBAC-only) is unsupported here.
2841
2942
``identity_federation_client_id`` is forwarded with whichever auth shape
3043
wins resolution. It selects mandatory SP-wide workload-identity token
@@ -54,14 +67,23 @@
5467
from typing import Any, Dict, Optional
5568

5669
from databricks.sql.auth.auth import (
70+
PYSQL_OAUTH_AZURE_CLIENT_ID,
71+
PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE,
5772
PYSQL_OAUTH_CLIENT_ID,
5873
PYSQL_OAUTH_REDIRECT_PORT_RANGE,
5974
PYSQL_OAUTH_SCOPES,
6075
)
6176
from databricks.sql.auth.authenticators import AccessTokenAuthProvider, AuthProvider
77+
from databricks.sql.auth.common import get_effective_azure_login_app_id
78+
from databricks.sql.auth.endpoint import AzureOAuthEndpointCollection
6279
from databricks.sql.auth.token_federation import TokenFederationProvider
6380
from databricks.sql.exc import NotSupportedError, ProgrammingError
6481

82+
# Entra (Azure AD) v2.0 token endpoint template. The kernel's generic M2M
83+
# provider sends the credentials as ``scope`` (v2.0), so we point it at the
84+
# v2.0 endpoint (the connector's own SP path uses the v1.0 ``resource`` form).
85+
_AZURE_AAD_LOGIN_HOST = "https://login.microsoftonline.com"
86+
6587
logger = logging.getLogger(__name__)
6688

6789

@@ -134,6 +156,7 @@ def _extract_bearer_token(auth_provider: Optional[AuthProvider]) -> Optional[str
134156
def kernel_auth_kwargs(
135157
auth_provider: Optional[AuthProvider],
136158
auth_options: Optional[Dict[str, Any]] = None,
159+
hostname: Optional[str] = None,
137160
) -> Dict[str, Any]:
138161
"""Build the kwargs passed to ``databricks_sql_kernel.Session(...)``.
139162
@@ -154,8 +177,9 @@ def kernel_auth_kwargs(
154177
- a U2M ``auth_type`` (``databricks-oauth``) *and*
155178
``oauth_client_secret`` together.
156179
157-
(``azure-oauth`` is rejected as unsupported before these guards —
158-
PECOBLR-4120.)
180+
(The Azure Entra auth types — ``azure-oauth`` and ``azure-sp-m2m`` —
181+
are routed to the kernel's generic OAuth flows up front, before these
182+
guards; see the module docstring.)
159183
1. **OAuth M2M** — ``oauth_client_id`` + ``oauth_client_secret``
160184
both present → forward raw creds to the kernel's ``oauth-m2m``.
161185
2. **PAT** — the built provider is (or wraps) an
@@ -168,7 +192,6 @@ def kernel_auth_kwargs(
168192
forwarding the connector's own OAuth app rather than the kernel's
169193
``databricks-sql-connector`` default (PECOBLR-4039/4040). Unlike the
170194
Thrift path, a caller-supplied ``oauth_scopes`` is honored here.
171-
``azure-oauth`` is rejected as unsupported (PECOBLR-4120).
172195
4. **Custom credentials_provider** → ``NotSupportedError`` (opaque
173196
token source; no raw creds for the kernel to own).
174197
5. Anything else → ``NotSupportedError``.
@@ -188,24 +211,87 @@ def kernel_auth_kwargs(
188211
auth_type = opts.get("auth_type")
189212
has_m2m = bool(client_id and client_secret)
190213

191-
# azure-oauth (Azure AD U2M) is not yet supported on the kernel path.
192-
# Reject it up front — before any M2M/U2M routing — so ANY azure-oauth
193-
# request gets a clear "not supported" error rather than being silently
194-
# misrouted (e.g. azure-oauth + client_id + secret would otherwise look
195-
# like M2M). The kernel resolves OAuth endpoints only from the
196-
# workspace-native OIDC config and has no Azure AD path, so the Thrift
197-
# azure-oauth flow (AAD token endpoint + /user_impersonation scope, see
198-
# AzureOAuthEndpointCollection) cannot be reproduced here. Forwarding an
199-
# azure bundle would authenticate against the wrong endpoints, so we fail
200-
# loudly at session-open. Tracked by PECOBLR-4120.
214+
# Azure Entra (Azure AD) auth types route to the kernel's GENERIC OAuth
215+
# flows with Azure values supplied as overrides — the kernel needs no
216+
# Azure-specific code. Handled up front, keyed on the explicit auth_type,
217+
# before the generic M2M/PAT/U2M routing below (azure-sp-m2m carries its
218+
# creds in azure_* kwargs, not oauth_client_id/secret, so it would
219+
# otherwise fall through to the final "unsupported" error).
220+
221+
# azure-oauth (Azure AD U2M): forward the Azure app bundle to oauth-u2m.
222+
# The kernel runs the browser flow and discovers endpoints via the
223+
# workspace /oidc redirector (which an Azure workspace redirects to Entra).
224+
# The AAD delegated scope ({app_id}/user_impersonation [+ offline_access])
225+
# is synthesised via AzureOAuthEndpointCollection, which also honors the
226+
# DATABRICKS_AZURE_TENANT_ID app-id override. PECOBLR-4120.
201227
if auth_type == "azure-oauth":
202-
raise NotSupportedError(
203-
"use_kernel=True does not support auth_type='azure-oauth' (Azure "
204-
"AD U2M) yet: the kernel resolves OAuth endpoints only from the "
205-
"workspace-native OIDC configuration and cannot drive the Azure AD "
206-
"authorization/token flow. Use the Thrift backend (default) for "
207-
"azure-oauth. Tracked by PECOBLR-4120."
228+
redirect_port = opts.get("oauth_redirect_port")
229+
caller_scopes = _normalize_scopes(opts.get("oauth_scopes"))
230+
mapped_scopes = AzureOAuthEndpointCollection().get_scopes_mapping(
231+
caller_scopes if caller_scopes is not None else list(PYSQL_OAUTH_SCOPES)
208232
)
233+
kwargs = {
234+
"auth_type": "oauth-u2m",
235+
"client_id": client_id or PYSQL_OAUTH_AZURE_CLIENT_ID,
236+
"redirect_ports": (
237+
[_coerce_redirect_port(redirect_port)]
238+
if client_id and redirect_port is not None
239+
else list(PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE)
240+
),
241+
"oauth_scopes": mapped_scopes,
242+
}
243+
if federation_client_id:
244+
kwargs["identity_federation_client_id"] = federation_client_id
245+
return kwargs
246+
247+
# azure-sp-m2m (Azure service principal, client-credentials): forward to
248+
# oauth-m2m with the Azure app credentials, an Entra v2.0 token endpoint,
249+
# and the {effective_app_id}/.default scope. The kernel sends the client
250+
# secret via HTTP Basic (which Entra v2.0 accepts) and, because a
251+
# token_url override is set, skips workspace OIDC discovery. PECOBLR-4141.
252+
#
253+
# NOT applied on the kernel path: the Azure management-token header
254+
# (X-Databricks-Azure-SP-Management-Token) and azure_workspace_resource_id.
255+
# No SQL connector (Go, Node) uses them; the Databricks-audience token
256+
# authenticates SPs that are workspace principals (the SQL norm). An SP with
257+
# only an Azure RBAC role (not a workspace member) is unsupported here.
258+
if auth_type == "azure-sp-m2m":
259+
azure_client_id = opts.get("azure_client_id")
260+
azure_client_secret = opts.get("azure_client_secret")
261+
azure_tenant_id = opts.get("azure_tenant_id")
262+
if not (azure_client_id and azure_client_secret):
263+
raise ProgrammingError(
264+
"auth_type='azure-sp-m2m' requires azure_client_id and "
265+
"azure_client_secret."
266+
)
267+
if not azure_tenant_id:
268+
# The Thrift path auto-discovers the tenant from the workspace's
269+
# /aad/auth redirect; the kernel path does not make that call, so
270+
# require it explicitly rather than silently guessing.
271+
raise NotSupportedError(
272+
"use_kernel=True auth_type='azure-sp-m2m' requires an explicit "
273+
"azure_tenant_id (the kernel path does not auto-discover the "
274+
"Azure tenant from the workspace as the Thrift backend does)."
275+
)
276+
if opts.get("azure_workspace_resource_id"):
277+
logger.warning(
278+
"azure_workspace_resource_id is ignored on use_kernel=True: the "
279+
"Azure management-token flow (X-Databricks-Azure-SP-Management-"
280+
"Token) is not applied on the kernel path. The Databricks-"
281+
"audience token authenticates service principals that are "
282+
"workspace principals; an RBAC-only SP is unsupported here."
283+
)
284+
app_id = get_effective_azure_login_app_id(hostname or "")
285+
kwargs = {
286+
"auth_type": "oauth-m2m",
287+
"client_id": azure_client_id,
288+
"client_secret": azure_client_secret,
289+
"token_url": f"{_AZURE_AAD_LOGIN_HOST}/{azure_tenant_id}/oauth2/v2.0/token",
290+
"oauth_scopes": [f"{app_id}/.default"],
291+
}
292+
if federation_client_id:
293+
kwargs["identity_federation_client_id"] = federation_client_id
294+
return kwargs
209295

210296
# 0. Ambiguity guards — fail before any flow is chosen.
211297
if client_secret and opts.get("credentials_provider") is not None:

src/databricks/sql/backend/kernel/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,11 @@ def open_session(
309309
auth_kwargs: Dict[str, Any] = {}
310310
tls_kwargs: Dict[str, Any] = {}
311311
try:
312-
auth_kwargs = kernel_auth_kwargs(self._auth_provider, self._auth_options)
312+
auth_kwargs = kernel_auth_kwargs(
313+
self._auth_provider,
314+
self._auth_options,
315+
hostname=self._server_hostname,
316+
)
313317
# Translate the connector's SSLOptions into the kernel's
314318
# ``tls_*`` Session kwargs. Empty when TLS is at defaults.
315319
tls_kwargs = _kernel_tls_kwargs(self._ssl_options)

0 commit comments

Comments
 (0)