Skip to content

Commit b2eed93

Browse files
fix(kernel): forward full OAuth U2M app bundle into kernel (PECOBLR-4040)
On the use_kernel path, OAuth U2M forwarded only whatever the caller explicitly set, sending a bare oauth-u2m otherwise. Since PECOBLR-4039 changed the kernel core default U2M app to databricks-sql-connector / sql offline_access / port 8030, a bare U2M connection authenticated as the wrong identity. The connector is an OVERRIDE of the kernel default, so it now forwards its full coupled bundle (client_id + oauth_scopes + redirect_port). Each field falls back to the connector's registered databricks-sql-python (or azure) default from the existing PYSQL_OAUTH_* constants, giving parity with the Thrift path. Explicit caller overrides and identity_federation_client_id forwarding are preserved. Co-authored-by: Isaac
1 parent 8f4daee commit b2eed93

3 files changed

Lines changed: 137 additions & 17 deletions

File tree

CHANGELOG.md

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

3+
# 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)
5+
36
# 4.4.0 (2026-07-22)
47
- 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)
58
- Fix: `REMOVE` staging operations no longer require `staging_allowed_local_path` to be set, since removing a remote file does not touch the local filesystem (databricks/databricks-sql-python#726)

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

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@
4848
import re
4949
from typing import Any, Dict, Optional
5050

51+
from databricks.sql.auth.auth import (
52+
PYSQL_OAUTH_AZURE_CLIENT_ID,
53+
PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE,
54+
PYSQL_OAUTH_CLIENT_ID,
55+
PYSQL_OAUTH_REDIRECT_PORT_RANGE,
56+
PYSQL_OAUTH_SCOPES,
57+
)
5158
from databricks.sql.auth.authenticators import AccessTokenAuthProvider, AuthProvider
5259
from databricks.sql.auth.token_federation import TokenFederationProvider
5360
from databricks.sql.exc import NotSupportedError, ProgrammingError
@@ -148,8 +155,14 @@ def kernel_auth_kwargs(
148155
2. **PAT** — the built provider is (or wraps) an
149156
``AccessTokenAuthProvider`` → extract the bearer token.
150157
3. **OAuth U2M** — ``auth_type`` is ``databricks-oauth`` /
151-
``azure-oauth`` → forward optional ``oauth_client_id`` /
152-
``oauth_redirect_port`` to the kernel's ``oauth-u2m``.
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).
153166
4. **Custom credentials_provider** → ``NotSupportedError`` (opaque
154167
token source; no raw creds for the kernel to own).
155168
5. Anything else → ``NotSupportedError``.
@@ -214,16 +227,51 @@ def kernel_auth_kwargs(
214227
return kwargs
215228

216229
# 3. OAuth U2M — browser authorization-code flow; the kernel runs it.
230+
#
231+
# The kernel's core default U2M app is databricks-sql-connector /
232+
# sql offline_access / port 8030 (PECOBLR-4039). The Python
233+
# connector is an OVERRIDE of that default: on this path we forward
234+
# its OWN full bundle rather than letting the kernel fall back to
235+
# the connector default. Forwarding a bare oauth-u2m would
236+
# authenticate as databricks-sql-connector, breaking parity with
237+
# the Thrift path (which authenticates as databricks-sql-python).
238+
#
239+
# client_id + scopes + redirect_port are coupled per OAuth app —
240+
# each app registers its own redirect URI — so all three are
241+
# resolved together: an explicit caller value wins; otherwise the
242+
# connector's registered databricks-sql-python (or azure) bundle is
243+
# used, mirroring the defaults get_python_sql_connector_auth_provider
244+
# applies on the Thrift path.
245+
#
246+
# Only the redirect PORT is routable into the kernel: it derives
247+
# http://localhost:{port}, with scheme/host/path fixed. The
248+
# connector registers a port *range* for its app but the kernel
249+
# accepts a single port, so we forward the first (canonical)
250+
# registered port.
217251
if auth_type in ("databricks-oauth", "azure-oauth"):
218-
kwargs = {"auth_type": "oauth-u2m"}
219-
if client_id:
220-
kwargs["client_id"] = client_id
252+
is_azure = auth_type == "azure-oauth"
253+
default_client_id = (
254+
PYSQL_OAUTH_AZURE_CLIENT_ID if is_azure else PYSQL_OAUTH_CLIENT_ID
255+
)
256+
default_port_range = (
257+
PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE
258+
if is_azure
259+
else PYSQL_OAUTH_REDIRECT_PORT_RANGE
260+
)
221261
redirect_port = opts.get("oauth_redirect_port")
222-
if redirect_port is not None:
223-
kwargs["redirect_port"] = int(redirect_port)
224262
scopes = _normalize_scopes(opts.get("oauth_scopes"))
225-
if scopes is not None:
226-
kwargs["oauth_scopes"] = scopes
263+
kwargs = {
264+
"auth_type": "oauth-u2m",
265+
"client_id": client_id or default_client_id,
266+
"redirect_port": (
267+
int(redirect_port)
268+
if redirect_port is not None
269+
else default_port_range[0]
270+
),
271+
"oauth_scopes": (
272+
scopes if scopes is not None else list(PYSQL_OAUTH_SCOPES)
273+
),
274+
}
227275
if federation_client_id:
228276
kwargs["identity_federation_client_id"] = federation_client_id
229277
return kwargs

tests/unit/test_kernel_auth_bridge.py

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
# require the kernel wheel). So this test can run on the
2727
# default-deps CI matrix without any extras. No importorskip needed.
2828

29+
from databricks.sql.auth.auth import (
30+
PYSQL_OAUTH_CLIENT_ID,
31+
PYSQL_OAUTH_AZURE_CLIENT_ID,
32+
PYSQL_OAUTH_SCOPES,
33+
PYSQL_OAUTH_REDIRECT_PORT_RANGE,
34+
PYSQL_OAUTH_AZURE_REDIRECT_PORT_RANGE,
35+
)
2936
from databricks.sql.auth.authenticators import (
3037
AccessTokenAuthProvider,
3138
AuthProvider,
@@ -240,31 +247,93 @@ def test_client_id_without_secret_does_not_trigger_m2m(self):
240247

241248

242249
class TestKernelOAuthU2M:
243-
@pytest.mark.parametrize("auth_type", ["databricks-oauth", "azure-oauth"])
244-
def test_u2m_routes_to_kernel_u2m(self, auth_type):
250+
"""The kernel core default U2M app is ``databricks-sql-connector`` /
251+
``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."""
259+
260+
def test_bare_databricks_oauth_forwards_full_python_bundle(self):
261+
# No overrides → forward the databricks-sql-python bundle in full
262+
# so the kernel does NOT fall back to its databricks-sql-connector
263+
# default. This is the parity-with-Thrift acceptance criterion.
264+
kwargs = kernel_auth_kwargs(
265+
_FakeOAuthProvider(),
266+
{"auth_type": "databricks-oauth"},
267+
)
268+
assert kwargs == {
269+
"auth_type": "oauth-u2m",
270+
"client_id": PYSQL_OAUTH_CLIENT_ID,
271+
"redirect_port": PYSQL_OAUTH_REDIRECT_PORT_RANGE[0],
272+
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
273+
}
274+
275+
def test_bare_azure_oauth_forwards_full_azure_bundle(self):
276+
kwargs = kernel_auth_kwargs(
277+
_FakeOAuthProvider(),
278+
{"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+
}
286+
287+
def test_u2m_custom_client_id_scopes_and_port_honored(self):
288+
# A caller overriding the app supplies the full coupled bundle;
289+
# every field is forwarded verbatim.
245290
kwargs = kernel_auth_kwargs(
246291
_FakeOAuthProvider(),
247-
{"auth_type": auth_type},
292+
{
293+
"auth_type": "databricks-oauth",
294+
"oauth_client_id": "custom-client",
295+
"oauth_scopes": ["custom-scope", "offline_access"],
296+
"oauth_redirect_port": 9999,
297+
},
248298
)
249-
assert kwargs == {"auth_type": "oauth-u2m"}
299+
assert kwargs == {
300+
"auth_type": "oauth-u2m",
301+
"client_id": "custom-client",
302+
"redirect_port": 9999,
303+
"oauth_scopes": ["custom-scope", "offline_access"],
304+
}
250305

251-
def test_u2m_forwards_client_id_and_redirect_port(self):
306+
def test_u2m_custom_client_id_only_falls_back_to_connector_defaults(self):
307+
# A custom client_id without explicit scopes/port fills the
308+
# remaining two from the connector defaults — matching the Thrift
309+
# path, where a custom client_id still uses PYSQL_OAUTH_SCOPES and
310+
# the default redirect-port range.
252311
kwargs = kernel_auth_kwargs(
253312
_FakeOAuthProvider(),
254313
{
255314
"auth_type": "databricks-oauth",
256315
"oauth_client_id": "custom-client",
257-
"oauth_redirect_port": 8030,
258316
},
259317
)
260318
assert kwargs == {
261319
"auth_type": "oauth-u2m",
262320
"client_id": "custom-client",
263-
"redirect_port": 8030,
321+
"redirect_port": PYSQL_OAUTH_REDIRECT_PORT_RANGE[0],
322+
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
264323
}
265324

325+
def test_u2m_redirect_port_coerced_to_int(self):
326+
# oauth_redirect_port may arrive as a string (e.g. from a DSN);
327+
# the kernel binding wants an int.
328+
kwargs = kernel_auth_kwargs(
329+
_FakeOAuthProvider(),
330+
{"auth_type": "databricks-oauth", "oauth_redirect_port": "8021"},
331+
)
332+
assert kwargs["redirect_port"] == 8021
333+
assert isinstance(kwargs["redirect_port"], int)
334+
266335
@pytest.mark.parametrize("auth_type", ["databricks-oauth", "azure-oauth"])
267-
def test_u2m_forwards_scopes(self, auth_type):
336+
def test_u2m_forwards_custom_scopes(self, auth_type):
268337
kwargs = kernel_auth_kwargs(
269338
_FakeOAuthProvider(),
270339
{"auth_type": auth_type, "oauth_scopes": ["all-apis", "offline_access"]},

0 commit comments

Comments
 (0)