Skip to content

Commit 311e3f0

Browse files
refactor(kernel): thin azure-sp-m2m — kernel owns Azure resolution + optional mgmt token
Make the `azure-sp-m2m` bridge thin, matching the kernel becoming the Azure-aware auth core. The connector now forwards `auth_type='azure-sp-m2m'` + `azure_client_id` / `azure_client_secret` (and optional `azure_tenant_id` / `azure_workspace_resource_id`) straight to the kernel Session, instead of constructing the Entra token endpoint and `{app_id}/.default` scope itself. Behavior changes on the kernel path (Thrift parity): - `azure_tenant_id` is now OPTIONAL — the kernel auto-discovers the tenant from the workspace's `/aad/auth` redirect when omitted, exactly as the Thrift backend does. (Previously the kernel path required it.) - `azure_workspace_resource_id` is now honored as an optional add-on: forward it and the kernel fetches an Azure-management token and sends the `X-Databricks-Azure-SP-Management-Token` + `X-Databricks-Azure-Workspace-Resource-Id` pair, so an SP with only an Azure RBAC role (not a workspace member) can authenticate. (Previously it was dropped with a warning.) Also thread the `azure_*` connection kwargs into `kernel_auth_options` in session.py — without this the bridge never received them and `azure-sp-m2m` failed at session-open with "requires azure_client_id". Adds a regression test for that threading, and rewrites the bridge tests for thin forwarding (tenant optional, resource id forwarded). Drops the now-unused `get_effective_azure_login_app_id` import and `_AZURE_AAD_LOGIN_HOST` constant. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
1 parent 01acb39 commit 311e3f0

6 files changed

Lines changed: 162 additions & 78 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Unreleased
44
- 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)
5+
- Kernel backend (`use_kernel=True`): **Azure Entra (Azure AD) OAuth is now supported.** The kernel is the Azure-aware auth core (it owns the endpoints, scopes, app ids, and tenant discovery); the connector forwards the selector and Azure credentials unchanged, so `connect()` is byte-identical between the Thrift and `use_kernel=True` paths. `auth_type="azure-oauth"` (Azure AD U2M) runs the kernel's browser flow against the workspace v2.0 authorize/token endpoints with the Azure app client id (`96eecda7-…`), redirect port `8030`, and the `{app_id}/user_impersonation offline_access` scope. `auth_type="azure-sp-m2m"` (Azure service principal) forwards `azure_client_id` / `azure_client_secret`; the kernel builds the Entra v2.0 token endpoint and the `{app_id}/.default` scope, and **auto-discovers the tenant** from the workspace's `/aad/auth` redirect when `azure_tenant_id` is omitted (matching Thrift). `azure_workspace_resource_id` is an optional add-on: set it and the kernel additionally sends the Azure management-token header pair (`X-Databricks-Azure-SP-Management-Token` + `X-Databricks-Azure-Workspace-Resource-Id`) so an SP that holds only an Azure RBAC role (not a workspace member) can authenticate; omit it and the SP authenticates with the data token alone (PECOBLR-4141; PECOBLR-4120)
66

77
# 4.4.0 (2026-07-22)
88
- 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)

CONNECTION_PARAMETERS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,16 @@ to change without notice.
6969
| Option | Type | Thrift | Kernel | Default Value | Note |
7070
| --------------------------------------------------- | -------------------- | :----: | :----: | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7171
| `access_token` (PAT) | `str` ||| `None` | Personal Access Token / bearer token. The default auth mode when set; otherwise auth falls back to OAuth. |
72-
| `auth_type` | `str` ||| `None` ⇒ Databricks OAuth | `databricks-oauth` (U2M), `azure-oauth` (Azure AD U2M), or `azure-sp-m2m` (Azure service-principal M2M). All three work on the kernel path; `azure-oauth` / `azure-sp-m2m` kernel support added in #919 (they route onto the kernel's generic OAuth flows with Azure values). |
72+
| `auth_type` | `str` ||| `None` ⇒ Databricks OAuth | `databricks-oauth` (U2M), `azure-oauth` (Azure AD U2M), or `azure-sp-m2m` (Azure service-principal M2M). All three work on the kernel path; `azure-oauth` / `azure-sp-m2m` kernel support added in #919 (the connector forwards the selector + Azure credentials and the kernel owns Azure resolution). |
7373
| `oauth_client_id` (U2M) | `str` ||| built-in client id | Custom U2M client id. Forwarded on both; when absent, each path applies its own built-in default. |
7474
| `oauth_redirect_port` (U2M) | `int` ||| `None` | Localhost redirect port for the browser flow. On **both** backends it is only honored when a custom `oauth_client_id` is also supplied — then that single port becomes the redirect URI. With the built-in client id (or when omitted) the connector uses the full registered range 8020–8024 and binds the first free port, so a bare `oauth_redirect_port` has no effect. (Thrift: `auth.py` `oauth_redirect_port_range`; Kernel: same logic, forwarded as `redirect_ports`.) |
7575
| `oauth_client_secret` (OAuth M2M) | `str` ||| `None` | **Kernel-only in practice.** The Thrift auth path never reads `oauth_client_secret`; use `credentials_provider` or an Azure service principal for M2M on Thrift. |
7676
| `oauth_scopes` | `List[str]` ||| `["sql","offline_access"]`| **Thrift ignores custom scopes** — it always uses the built-in scope set. Only the kernel honors a custom `oauth_scopes`. |
7777
| `credentials_provider` | `CredentialsProvider`||| `None` | Custom external credentials provider. **Rejected on the kernel path** (`NotSupportedError`) — it is an opaque token source, so the kernel cannot own the token lifecycle; use `oauth_client_id` + `oauth_client_secret` for M2M, or the Thrift backend. |
7878
| `identity_federation_client_id` | `str` ||| `None` | Workload identity / token-federation client id (kernel support added in #910). |
7979
| `experimental_oauth_persistence` | `OAuthPersistence` ||| `None` | **Thrift-only.** The kernel owns its own token lifecycle and does not accept a persistence store. |
80-
| `azure_client_id` / `azure_client_secret` / `azure_tenant_id` | `str` ||| `None` | Azure service-principal (Entra ID M2M), selected by `auth_type="azure-sp-m2m"`. On the kernel path these route onto OAuth M2M with an Entra v2.0 token endpoint + the Databricks-resource `.default` scope (#919). **`azure_tenant_id` is required on the kernel path**unlike Thrift, it is not auto-discovered from the workspace. |
81-
| `azure_workspace_resource_id` | `str` || ⚠️ | `None` | Thrift sends this with the Azure SP **management token** (`X-Databricks-Azure-SP-Management-Token`) to authorize an SP that has an Azure RBAC role but is not a workspace member. **Not applied on the kernel path**the management-token flow is unsupported there (matching the Go and Node SQL drivers, which don't use it); add the SP as a workspace principal instead. Setting it on the kernel path logs a warning and is otherwise ignored. |
80+
| `azure_client_id` / `azure_client_secret` / `azure_tenant_id` | `str` ||| `None` | Azure service-principal (Entra ID M2M), selected by `auth_type="azure-sp-m2m"`. On the kernel path the connector forwards these to the kernel, which owns Azure resolution (Entra v2.0 token endpoint + the Databricks-resource `.default` scope) (#919). **`azure_tenant_id` is optional on the kernel path too**like Thrift, the kernel auto-discovers it from the workspace's `/aad/auth` redirect when omitted. |
81+
| `azure_workspace_resource_id` | `str` || | `None` | Optional add-on for `azure-sp-m2m`. When set, the SP **management token** (`X-Databricks-Azure-SP-Management-Token` + `X-Databricks-Azure-Workspace-Resource-Id`) is sent to authorize an SP that has an Azure RBAC role but is not a workspace member. On the kernel path the connector forwards it and the kernel fetches the management token and emits the header pair; omit it and the SP authenticates with the Databricks-audience data token alone. |
8282
| `_use_cert_as_auth` (+ `_tls_client_cert_file`) | `bool` ||| `False` | Authenticate with a TLS client certificate instead of a token. Thrift-only. |
8383
| `username` / `password` | `str` ||| `None` | **Removed.** Basic auth is no longer supported; passing either raises `ValueError`. |
8484

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

Lines changed: 45 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,25 @@
2121
``oauth_client_id`` / ``oauth_redirect_port`` overriding it) is
2222
forwarded to the kernel's ``auth_type='oauth-u2m'`` and the kernel
2323
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.
24+
- **Azure Entra (Azure AD)** — both Azure auth types forward the selector
25+
and Azure credentials to the KERNEL, which is the Azure-aware auth core
26+
(it owns the endpoints, scopes, app ids, and tenant discovery). The
27+
binding stays thin — it does not construct endpoints or scopes:
28+
29+
- ``azure-oauth`` (U2M) → forward ``auth_type='azure-oauth'`` (plus any
30+
optional ``oauth_client_id`` / ``oauth_redirect_port`` passthrough). The
31+
kernel pins the workspace v2.0 authorize/token endpoints, the Azure app
32+
client id (``96eecda7-…``), port ``8030``, and the
33+
``{app_id}/user_impersonation offline_access`` scope (PECOBLR-4120).
34+
- ``azure-sp-m2m`` (M2M) → forward ``auth_type='azure-sp-m2m'`` with the
35+
Azure service-principal ``azure_client_id`` / ``azure_client_secret``.
36+
The kernel builds the Entra v2.0 token endpoint and the
37+
``{effective_app_id}/.default`` scope, and auto-discovers the tenant from
38+
the workspace's ``/aad/auth`` redirect when ``azure_tenant_id`` is omitted
39+
(Thrift parity). ``azure_workspace_resource_id`` is an optional add-on:
40+
forward it and the kernel additionally sends the Azure management-token
41+
header pair, so an RBAC-only SP (not a workspace member) can authenticate
42+
(PECOBLR-4141).
4143
4244
``identity_federation_client_id`` is forwarded with whichever auth shape
4345
wins resolution. It selects mandatory SP-wide workload-identity token
@@ -72,15 +74,9 @@
7274
PYSQL_OAUTH_SCOPES,
7375
)
7476
from databricks.sql.auth.authenticators import AccessTokenAuthProvider, AuthProvider
75-
from databricks.sql.auth.common import get_effective_azure_login_app_id
7677
from databricks.sql.auth.token_federation import TokenFederationProvider
7778
from databricks.sql.exc import NotSupportedError, ProgrammingError
7879

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

8682

@@ -175,7 +171,7 @@ def kernel_auth_kwargs(
175171
``oauth_client_secret`` together.
176172
177173
(The Azure Entra auth types — ``azure-oauth`` and ``azure-sp-m2m`` —
178-
are routed to the kernel's generic OAuth flows up front, before these
174+
are forwarded to the kernel's Azure-aware flows up front, before these
179175
guards; see the module docstring.)
180176
1. **OAuth M2M** — ``oauth_client_id`` + ``oauth_client_secret``
181177
both present → forward raw creds to the kernel's ``oauth-m2m``.
@@ -234,51 +230,42 @@ def kernel_auth_kwargs(
234230
kwargs["identity_federation_client_id"] = federation_client_id
235231
return kwargs
236232

237-
# azure-sp-m2m (Azure service principal, client-credentials): forward to
238-
# oauth-m2m with the Azure app credentials, an Entra v2.0 token endpoint,
239-
# and the {effective_app_id}/.default scope. The kernel sends the client
240-
# secret via HTTP Basic (which Entra v2.0 accepts) and, because a
241-
# token_url override is set, skips workspace OIDC discovery. PECOBLR-4141.
233+
# azure-sp-m2m (Azure service principal, client-credentials): forward the
234+
# selector + Azure SP credentials; the KERNEL owns Azure resolution (it is
235+
# the auth core). The kernel builds the Entra v2.0 token endpoint
236+
# (`{login}/{tenant}/oauth2/v2.0/token`) and the `{effective_app_id}/.default`
237+
# scope, and — when azure_tenant_id is omitted — auto-discovers the tenant
238+
# from the workspace's /aad/auth redirect, matching the Thrift backend
239+
# (so connect() is byte-identical between Thrift and use_kernel=True).
240+
# PECOBLR-4141.
242241
#
243-
# NOT applied on the kernel path: the Azure management-token header
244-
# (X-Databricks-Azure-SP-Management-Token) and azure_workspace_resource_id.
245-
# No SQL connector (Go, Node) uses them; the Databricks-audience token
246-
# authenticates SPs that are workspace principals (the SQL norm). An SP with
247-
# only an Azure RBAC role (not a workspace member) is unsupported here.
242+
# azure_workspace_resource_id is an optional add-on: forward it and the
243+
# kernel additionally fetches an Azure-management token and sends the
244+
# X-Databricks-Azure-SP-Management-Token + X-Databricks-Azure-Workspace-
245+
# Resource-Id pair, so an SP that holds only an Azure RBAC role (not a
246+
# workspace member) can authenticate. Omit it (the common case) and the SP
247+
# authenticates with the Databricks-audience data token alone.
248248
if auth_type == "azure-sp-m2m":
249249
azure_client_id = opts.get("azure_client_id")
250250
azure_client_secret = opts.get("azure_client_secret")
251-
azure_tenant_id = opts.get("azure_tenant_id")
252251
if not (azure_client_id and azure_client_secret):
253252
raise ProgrammingError(
254253
"auth_type='azure-sp-m2m' requires azure_client_id and "
255254
"azure_client_secret."
256255
)
257-
if not azure_tenant_id:
258-
# The Thrift path auto-discovers the tenant from the workspace's
259-
# /aad/auth redirect; the kernel path does not make that call, so
260-
# require it explicitly rather than silently guessing.
261-
raise NotSupportedError(
262-
"use_kernel=True auth_type='azure-sp-m2m' requires an explicit "
263-
"azure_tenant_id (the kernel path does not auto-discover the "
264-
"Azure tenant from the workspace as the Thrift backend does)."
265-
)
266-
if opts.get("azure_workspace_resource_id"):
267-
logger.warning(
268-
"azure_workspace_resource_id is ignored on use_kernel=True: the "
269-
"Azure management-token flow (X-Databricks-Azure-SP-Management-"
270-
"Token) is not applied on the kernel path. The Databricks-"
271-
"audience token authenticates service principals that are "
272-
"workspace principals; an RBAC-only SP is unsupported here."
273-
)
274-
app_id = get_effective_azure_login_app_id(hostname or "")
275256
kwargs = {
276-
"auth_type": "oauth-m2m",
277-
"client_id": azure_client_id,
278-
"client_secret": azure_client_secret,
279-
"token_url": f"{_AZURE_AAD_LOGIN_HOST}/{azure_tenant_id}/oauth2/v2.0/token",
280-
"oauth_scopes": [f"{app_id}/.default"],
257+
"auth_type": "azure-sp-m2m",
258+
"azure_client_id": azure_client_id,
259+
"azure_client_secret": azure_client_secret,
281260
}
261+
# Optional passthroughs: the kernel auto-discovers the tenant when
262+
# absent, and sends the data token alone when no resource id is set.
263+
azure_tenant_id = opts.get("azure_tenant_id")
264+
if azure_tenant_id:
265+
kwargs["azure_tenant_id"] = azure_tenant_id
266+
azure_workspace_resource_id = opts.get("azure_workspace_resource_id")
267+
if azure_workspace_resource_id:
268+
kwargs["azure_workspace_resource_id"] = azure_workspace_resource_id
282269
if federation_client_id:
283270
kwargs["identity_federation_client_id"] = federation_client_id
284271
return kwargs

src/databricks/sql/session.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,19 @@ def _create_backend(
177177
"identity_federation_client_id": kwargs.get(
178178
"identity_federation_client_id"
179179
),
180+
# Azure Entra SP credentials for the azure-sp-m2m path. The
181+
# kernel owns Azure resolution (endpoint/scope/tenant discovery),
182+
# so these raw kwargs are the only source; without threading them
183+
# the bridge would fail with "requires azure_client_id". The
184+
# tenant and workspace-resource-id are optional (kernel
185+
# auto-discovers the tenant; the resource id gates the optional
186+
# management token). Kernel-only; Thrift / SEA are unaffected.
187+
"azure_client_id": kwargs.get("azure_client_id"),
188+
"azure_client_secret": kwargs.get("azure_client_secret"),
189+
"azure_tenant_id": kwargs.get("azure_tenant_id"),
190+
"azure_workspace_resource_id": kwargs.get(
191+
"azure_workspace_resource_id"
192+
),
180193
}
181194
# Forward the connector's retry-tuning kwargs so the kernel's
182195
# own retry policy honours them (the kernel owns the retry

0 commit comments

Comments
 (0)