Skip to content

Commit bc6702f

Browse files
fix(kernel): don't handle azure-oauth U2M yet; reject it (PECOBLR-4120)
Azure AD U2M can't work through the kernel today: the kernel resolves OAuth endpoints only from the workspace-native OIDC config and has no Azure AD path, so the Thrift azure-oauth flow (AAD token endpoint + /user_impersonation scope) cannot be reproduced. Rather than forward an azure bundle that authenticates against the wrong endpoints, reject auth_type='azure-oauth' up front with a clear NotSupportedError pointing at the Thrift backend. The kernel U2M path now handles databricks-oauth only. Azure support is tracked by PECOBLR-4120. Also fixes the stale scope tests the prior review left red: scopes are hardcoded to PYSQL_OAUTH_SCOPES for Thrift parity (not caller- overridable), and the tests now assert that. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
1 parent fdc20e5 commit bc6702f

3 files changed

Lines changed: 103 additions & 78 deletions

File tree

CHANGELOG.md

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

33
# Unreleased
4-
- Kernel backend (`use_kernel=True`): OAuth U2M now forwards the connector's full OAuth-app bundle (`client_id` + scopes + redirect port) into the kernel, so a bare U2M connection authenticates as `databricks-sql-python` with `sql offline_access` — parity with the Thrift path — instead of inheriting the kernel's own `databricks-sql-connector` default. Caller-supplied `oauth_client_id` / `oauth_scopes` / `oauth_redirect_port` are still honored (PECOBLR-4040)
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; scopes are fixed to match the Thrift path. `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)
55

66
# 4.4.0 (2026-07-22)
77
- 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: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
connector's own OAuth provider because the kernel re-mints tokens
1616
itself and the client secret is not recoverable from a built
1717
provider.
18-
- **OAuth U2M** — for ``auth_type`` ``databricks-oauth`` /
19-
``azure-oauth`` (the browser authorization-code flow), the optional
20-
``oauth_client_id`` / ``oauth_redirect_port`` are forwarded to the
21-
kernel's ``auth_type='oauth-u2m'`` and the kernel runs the browser
22-
flow itself.
18+
- **OAuth U2M** — for ``auth_type`` ``databricks-oauth`` (the browser
19+
authorization-code flow), the connector's ``databricks-sql-python``
20+
app bundle (``client_id`` + ``redirect_port``, with the optional
21+
``oauth_client_id`` / ``oauth_redirect_port`` overriding it) is
22+
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).
2328
2429
``identity_federation_client_id`` is forwarded with whichever auth shape
2530
wins resolution. It selects mandatory SP-wide workload-identity token
@@ -49,8 +54,6 @@
4954
from typing import Any, Dict, Optional
5055

5156
from databricks.sql.auth.auth import (
52-
PYSQL_OAUTH_AZURE_CLIENT_ID,
53-
PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE,
5457
PYSQL_OAUTH_CLIENT_ID,
5558
PYSQL_OAUTH_REDIRECT_PORT_RANGE,
5659
PYSQL_OAUTH_SCOPES,
@@ -148,21 +151,23 @@ def kernel_auth_kwargs(
148151
rather than silently picking one flow (and failing later as a
149152
confusing 401 against the wrong principal):
150153
- a custom ``credentials_provider`` *and* M2M kwargs together;
151-
- a U2M ``auth_type`` (``databricks-oauth`` / ``azure-oauth``)
152-
*and* ``oauth_client_secret`` together.
154+
- a U2M ``auth_type`` (``databricks-oauth``) *and*
155+
``oauth_client_secret`` together.
156+
157+
(``azure-oauth`` is rejected as unsupported before these guards —
158+
PECOBLR-4120.)
153159
1. **OAuth M2M** — ``oauth_client_id`` + ``oauth_client_secret``
154160
both present → forward raw creds to the kernel's ``oauth-m2m``.
155161
2. **PAT** — the built provider is (or wraps) an
156162
``AccessTokenAuthProvider`` → extract the bearer token.
157-
3. **OAuth U2M** — ``auth_type`` is ``databricks-oauth`` /
158-
``azure-oauth`` → forward the connector's *full* coupled OAuth-app
159-
bundle (``client_id`` + ``oauth_scopes`` + ``redirect_port``) to
160-
the kernel's ``oauth-u2m``. Each field falls back to the
161-
connector's registered ``databricks-sql-python`` (or azure)
162-
default when the caller doesn't override it, so a bare U2M
163-
connection authenticates as ``databricks-sql-python`` — parity
164-
with the Thrift path — rather than the kernel's own
165-
``databricks-sql-connector`` default (PECOBLR-4039/4040).
163+
3. **OAuth U2M** — ``auth_type`` is ``databricks-oauth`` → forward the
164+
connector's coupled ``databricks-sql-python`` bundle (``client_id``
165+
+ ``redirect_port``, with fixed ``PYSQL_OAUTH_SCOPES``) to the
166+
kernel's ``oauth-u2m``, so a bare U2M connection authenticates as
167+
``databricks-sql-python`` — parity with the Thrift path — rather
168+
than the kernel's own ``databricks-sql-connector`` default
169+
(PECOBLR-4039/4040). ``azure-oauth`` is rejected as unsupported
170+
(PECOBLR-4120).
166171
4. **Custom credentials_provider** → ``NotSupportedError`` (opaque
167172
token source; no raw creds for the kernel to own).
168173
5. Anything else → ``NotSupportedError``.
@@ -182,6 +187,25 @@ def kernel_auth_kwargs(
182187
auth_type = opts.get("auth_type")
183188
has_m2m = bool(client_id and client_secret)
184189

190+
# azure-oauth (Azure AD U2M) is not yet supported on the kernel path.
191+
# Reject it up front — before any M2M/U2M routing — so ANY azure-oauth
192+
# request gets a clear "not supported" error rather than being silently
193+
# misrouted (e.g. azure-oauth + client_id + secret would otherwise look
194+
# like M2M). The kernel resolves OAuth endpoints only from the
195+
# workspace-native OIDC config and has no Azure AD path, so the Thrift
196+
# azure-oauth flow (AAD token endpoint + /user_impersonation scope, see
197+
# AzureOAuthEndpointCollection) cannot be reproduced here. Forwarding an
198+
# azure bundle would authenticate against the wrong endpoints, so we fail
199+
# loudly at session-open. Tracked by PECOBLR-4120.
200+
if auth_type == "azure-oauth":
201+
raise NotSupportedError(
202+
"use_kernel=True does not support auth_type='azure-oauth' (Azure "
203+
"AD U2M) yet: the kernel resolves OAuth endpoints only from the "
204+
"workspace-native OIDC configuration and cannot drive the Azure AD "
205+
"authorization/token flow. Use the Thrift backend (default) for "
206+
"azure-oauth. Tracked by PECOBLR-4120."
207+
)
208+
185209
# 0. Ambiguity guards — fail before any flow is chosen.
186210
if client_secret and opts.get("credentials_provider") is not None:
187211
raise NotSupportedError(
@@ -191,7 +215,7 @@ def kernel_auth_kwargs(
191215
"kernel-managed M2M, or use the Thrift backend (default) for "
192216
"credentials_provider."
193217
)
194-
if client_secret and auth_type in ("databricks-oauth", "azure-oauth"):
218+
if client_secret and auth_type == "databricks-oauth":
195219
raise NotSupportedError(
196220
f"Ambiguous auth on use_kernel=True: auth_type={auth_type!r} selects "
197221
"the U2M browser flow, but oauth_client_secret was also provided "
@@ -227,6 +251,8 @@ def kernel_auth_kwargs(
227251
return kwargs
228252

229253
# 3. OAuth U2M — browser authorization-code flow; the kernel runs it.
254+
# Only databricks-oauth reaches here (azure-oauth was rejected up
255+
# front — see the guard near the top of this function).
230256
#
231257
# The kernel's core default U2M app is databricks-sql-connector /
232258
# sql offline_access / port 8030 (PECOBLR-4039). The Python
@@ -239,12 +265,12 @@ def kernel_auth_kwargs(
239265
# client_id + redirect_port are coupled per OAuth app — each app
240266
# registers its own redirect URI — so both are resolved together:
241267
# an explicit caller value wins; otherwise the connector's
242-
# registered databricks-sql-python (or azure) bundle is used,
243-
# mirroring the defaults get_python_sql_connector_auth_provider
244-
# applies on the Thrift path. scopes are NOT caller-overridable:
245-
# the Thrift path hardcodes PYSQL_OAUTH_SCOPES for U2M (a caller's
246-
# oauth_scopes kwarg is never read there), so we forward the same
247-
# fixed scopes here to keep the two backends in parity.
268+
# registered databricks-sql-python bundle is used, mirroring the
269+
# defaults get_python_sql_connector_auth_provider applies on the
270+
# Thrift path. scopes are NOT caller-overridable: the Thrift path
271+
# hardcodes PYSQL_OAUTH_SCOPES for U2M (a caller's oauth_scopes
272+
# kwarg is never read there), so we forward the same fixed scopes
273+
# here to keep the two backends in parity.
248274
#
249275
# Only the redirect PORT is routable into the kernel: it derives
250276
# http://localhost:{port}, with scheme/host/path fixed. The
@@ -255,16 +281,7 @@ def kernel_auth_kwargs(
255281
# the Thrift path's coupling (a bare oauth_redirect_port paired
256282
# with the default databricks-sql-python app would resolve to an
257283
# unregistered redirect URI and fail the flow).
258-
if auth_type in ("databricks-oauth", "azure-oauth"):
259-
is_azure = auth_type == "azure-oauth"
260-
default_client_id = (
261-
PYSQL_OAUTH_AZURE_CLIENT_ID if is_azure else PYSQL_OAUTH_CLIENT_ID
262-
)
263-
default_port_range = (
264-
PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE
265-
if is_azure
266-
else PYSQL_OAUTH_REDIRECT_PORT_RANGE
267-
)
284+
if auth_type == "databricks-oauth":
268285
redirect_port = opts.get("oauth_redirect_port")
269286
# Validate any caller-supplied oauth_scopes (a bad type is still a
270287
# caller error worth flagging) but do NOT forward it: the Thrift
@@ -274,11 +291,11 @@ def kernel_auth_kwargs(
274291
_normalize_scopes(opts.get("oauth_scopes"))
275292
kwargs = {
276293
"auth_type": "oauth-u2m",
277-
"client_id": client_id or default_client_id,
294+
"client_id": client_id or PYSQL_OAUTH_CLIENT_ID,
278295
"redirect_port": (
279296
int(redirect_port)
280297
if client_id and redirect_port is not None
281-
else default_port_range[0]
298+
else PYSQL_OAUTH_REDIRECT_PORT_RANGE[0]
282299
),
283300
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
284301
}
@@ -309,7 +326,7 @@ def kernel_auth_kwargs(
309326
raise NotSupportedError(
310327
f"use_kernel=True requires PAT (access_token), OAuth M2M "
311328
f"(oauth_client_id + oauth_client_secret), or OAuth U2M "
312-
f"(auth_type='databricks-oauth' / 'azure-oauth'), but got "
329+
f"(auth_type='databricks-oauth'), but got "
313330
f"{provider_desc} with auth_type={auth_type!r}. Use the Thrift "
314331
"backend (default) for other auth flows."
315332
)

tests/unit/test_kernel_auth_bridge.py

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
look through the wrapper).
99
- OAuth M2M (``oauth_client_id`` + ``oauth_client_secret``) routes
1010
through ``auth_type='oauth-m2m'`` with the raw creds forwarded.
11-
- OAuth U2M (``auth_type='databricks-oauth'`` / ``'azure-oauth'``)
12-
routes through ``auth_type='oauth-u2m'``.
11+
- OAuth U2M (``auth_type='databricks-oauth'``) routes through
12+
``auth_type='oauth-u2m'``. ``azure-oauth`` (Azure AD) is not yet
13+
supported on the kernel path and is rejected (PECOBLR-4120).
1314
- A custom ``credentials_provider`` and any other non-PAT shape raise
1415
``NotSupportedError`` with a clear, actionable message.
1516
"""
@@ -28,10 +29,8 @@
2829

2930
from databricks.sql.auth.auth import (
3031
PYSQL_OAUTH_CLIENT_ID,
31-
PYSQL_OAUTH_AZURE_CLIENT_ID,
3232
PYSQL_OAUTH_SCOPES,
3333
PYSQL_OAUTH_REDIRECT_PORT_RANGE,
34-
PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE,
3534
)
3635
from databricks.sql.auth.authenticators import (
3736
AccessTokenAuthProvider,
@@ -247,15 +246,19 @@ def test_client_id_without_secret_does_not_trigger_m2m(self):
247246

248247

249248
class TestKernelOAuthU2M:
250-
"""The kernel core default U2M app is ``databricks-sql-connector`` /
249+
"""Only ``databricks-oauth`` U2M is supported on the kernel path.
250+
251+
The kernel core default U2M app is ``databricks-sql-connector`` /
251252
``sql offline_access`` / port 8030 (see PECOBLR-4039). The Python
252-
connector is an OVERRIDE: on the kernel path it must forward its OWN
253-
full bundle — ``client_id`` + ``oauth_scopes`` + ``redirect_port`` —
254-
because the three are coupled per OAuth app. Forwarding a partial
255-
bundle would let the kernel fill the rest from the connector default,
256-
authenticating as the wrong principal / against an unregistered
257-
redirect URI. So bare U2M must forward the complete
258-
``databricks-sql-python`` bundle for parity with the Thrift path."""
253+
connector is an OVERRIDE: on the kernel path it forwards its OWN
254+
coupled ``client_id`` + ``redirect_port`` bundle so it authenticates
255+
as ``databricks-sql-python`` rather than the kernel default. Scopes
256+
are fixed to ``PYSQL_OAUTH_SCOPES`` (not caller-overridable), matching
257+
the Thrift path which hardcodes them for U2M.
258+
259+
``azure-oauth`` (Azure AD) is deliberately NOT handled yet — the
260+
kernel can't drive the Azure AD authorization/token flow — so it is
261+
rejected up front (PECOBLR-4120)."""
259262

260263
def test_bare_databricks_oauth_forwards_full_python_bundle(self):
261264
# No overrides → forward the databricks-sql-python bundle in full
@@ -272,25 +275,28 @@ def test_bare_databricks_oauth_forwards_full_python_bundle(self):
272275
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
273276
}
274277

275-
def test_bare_azure_oauth_forwards_full_azure_bundle(self):
276-
kwargs = kernel_auth_kwargs(
277-
_FakeOAuthProvider(),
278+
@pytest.mark.parametrize(
279+
"opts",
280+
[
278281
{"auth_type": "azure-oauth"},
279-
)
280-
assert kwargs == {
281-
"auth_type": "oauth-u2m",
282-
"client_id": PYSQL_OAUTH_AZURE_CLIENT_ID,
283-
"redirect_port": PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE[0],
284-
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
285-
}
282+
{"auth_type": "azure-oauth", "oauth_client_id": "custom"},
283+
{"auth_type": "azure-oauth", "oauth_redirect_port": 8030},
284+
],
285+
ids=["bare", "with_client_id", "with_port"],
286+
)
287+
def test_azure_oauth_not_supported(self, opts):
288+
# azure-oauth (Azure AD U2M) can't work through the kernel yet: the
289+
# kernel resolves OAuth endpoints only from workspace-native OIDC
290+
# discovery and has no Azure AD path. Fail loudly at session-open
291+
# rather than forwarding a bundle that authenticates against the
292+
# wrong endpoints. Tracked by PECOBLR-4120.
293+
with pytest.raises(NotSupportedError, match="azure-oauth"):
294+
kernel_auth_kwargs(_FakeOAuthProvider(), opts)
286295

287296
def test_u2m_custom_client_id_and_port_honored_scopes_fixed(self):
288-
# A caller overriding the app supplies the coupled client_id +
289-
# redirect_port, which are forwarded verbatim. oauth_scopes is NOT
290-
# caller-overridable: the Thrift path hardcodes PYSQL_OAUTH_SCOPES
291-
# for U2M (it never reads an oauth_scopes kwarg), so the kernel
292-
# path forwards the same fixed scopes for parity even when the
293-
# caller passes their own.
297+
# A caller may override the coupled client_id + redirect_port. Scopes
298+
# are NOT caller-overridable (Thrift parity): a supplied oauth_scopes
299+
# is ignored and PYSQL_OAUTH_SCOPES is forwarded regardless.
294300
kwargs = kernel_auth_kwargs(
295301
_FakeOAuthProvider(),
296302
{
@@ -356,15 +362,17 @@ def test_u2m_redirect_port_ignored_without_client_id(self):
356362
)
357363
assert kwargs["redirect_port"] == PYSQL_OAUTH_REDIRECT_PORT_RANGE[0]
358364

359-
@pytest.mark.parametrize("auth_type", ["databricks-oauth", "azure-oauth"])
360-
def test_u2m_ignores_custom_scopes_for_thrift_parity(self, auth_type):
361-
# The Thrift path hardcodes PYSQL_OAUTH_SCOPES for U2M and never
362-
# reads a caller's oauth_scopes; the kernel path forwards the same
363-
# fixed scopes for parity rather than honoring an override the
364-
# other backend silently ignores.
365+
def test_u2m_ignores_custom_scopes(self):
366+
# Scopes are fixed for U2M — the Thrift path hardcodes
367+
# PYSQL_OAUTH_SCOPES and never reads a caller oauth_scopes kwarg, so
368+
# the kernel path forwards the same fixed scopes for parity. A
369+
# (well-typed) caller oauth_scopes is validated but not honored.
365370
kwargs = kernel_auth_kwargs(
366371
_FakeOAuthProvider(),
367-
{"auth_type": auth_type, "oauth_scopes": ["all-apis", "offline_access"]},
372+
{
373+
"auth_type": "databricks-oauth",
374+
"oauth_scopes": ["all-apis", "offline_access"],
375+
},
368376
)
369377
assert kwargs["oauth_scopes"] == list(PYSQL_OAUTH_SCOPES)
370378

@@ -428,15 +436,15 @@ def _creds_provider():
428436
},
429437
)
430438

431-
@pytest.mark.parametrize("auth_type", ["databricks-oauth", "azure-oauth"])
432-
def test_u2m_auth_type_plus_client_secret_is_rejected(self, auth_type):
439+
def test_u2m_auth_type_plus_client_secret_is_rejected(self):
433440
# User asked for U2M (browser) but also passed a secret (M2M).
434-
# Don't silently route M2M against the wrong principal.
441+
# Don't silently route M2M against the wrong principal. (azure-oauth
442+
# is rejected earlier as unsupported, so it's not exercised here.)
435443
with pytest.raises(NotSupportedError, match="Ambiguous auth"):
436444
kernel_auth_kwargs(
437445
_FakeOAuthProvider(),
438446
{
439-
"auth_type": auth_type,
447+
"auth_type": "databricks-oauth",
440448
"oauth_client_id": "id",
441449
"oauth_client_secret": "sec",
442450
},

0 commit comments

Comments
 (0)