Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Release History

# Unreleased
- Kernel backend (`use_kernel=True`): Reject attempts to set `spark.thriftserver.arrowBasedRowSet.timestampAsString` to a non-false value during session creation, matching the Thrift backend's validation.
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
Outdated
- Kernel backend (`use_kernel=True`): OAuth **M2M with a JWT private-key client assertion** (RFC 7523) is now supported. Pass `oauth_client_id` + `oauth_jwt_key_file` + `oauth_jwt_kid` (with optional `oauth_jwt_passphrase` for an encrypted PKCS#8 key, `oauth_jwt_algorithm` defaulting to `RS256`, `oauth_scopes`, and `token_url` for the IdP token endpoint) and the connector routes them to the kernel's `auth_type="oauth-m2m-jwt"`, which signs a short-lived assertion with the private key instead of sending a client secret. The kernel owns the token lifecycle. A private-key file is treated as unambiguous JWT M2M intent and is mutually exclusive with `oauth_client_secret` / `credentials_provider` (both raise `NotSupportedError`). Verified end-to-end against an Azure Databricks workspace with the service principal's public certificate registered on its Entra ID app registration. Requires `databricks-sql-kernel >= 0.2.0` with JWT support.
- 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)
- Kernel backend (`use_kernel=True`): **Azure Entra (Azure AD) service-principal M2M is now supported.** `auth_type="azure-sp-m2m"` forwards `azure_client_id` / `azure_client_secret`; the kernel is the Azure-aware auth core — it 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). The `Authorization` bearer is the Databricks-audience data token, which alone authenticates a workspace-member SP. Set `azure_workspace_resource_id` and the kernel also sends the Azure SP management token (`X-Databricks-Azure-SP-Management-Token`) + `X-Databricks-Azure-Workspace-Resource-Id` header (matching the JDBC driver), so a service principal with an Azure RBAC role but no workspace membership can authenticate; omit it and no ARM management-scope token is fetched. Azure AD **U2M** (`auth_type="azure-oauth"`) now routes to the kernel's OAuth U2M flow, identically to `auth_type="databricks-oauth"`: the kernel runs the in-house workspace-federated browser flow, which Azure workspaces support (the workspace federates login to Entra). It forwards the connector's `databricks-sql-python` OAuth app, not the Thrift Azure app (`96eecda7` / port 8030), which is registered for Thrift's direct-Entra flow the kernel does not perform (PECOBLR-4141; PECOBLR-4120)
Expand Down
21 changes: 15 additions & 6 deletions src/databricks/sql/backend/kernel/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,16 +310,25 @@ def open_session(
session_conf: Optional[Dict[str, str]] = None
if session_configuration:
session_conf = {k: str(v) for k, v in session_configuration.items()}
# The kwarg builds run INSIDE the try so the ``finally`` scrub
# below always fires — including when ``kernel_auth_kwargs``
# The kwarg builds — and the session-conf validation — run INSIDE
# the try so the ``finally`` scrub below always fires, including
# when the validation rejects or when ``kernel_auth_kwargs``
# itself raises mid-build (e.g. an OAuth token-exchange failure
# while the M2M secret is in hand). Pre-declared empty so the
# ``finally`` can reference them unconditionally even on an early
# raise. Building here (not in ``__init__``) keeps the bearer
# token's in-process lifetime as short as possible.
# while the M2M secret is in hand). ``self._auth_options`` already
# holds the secret at this point (set in ``__init__``), so an
# early raise before the try would leave it un-scrubbed. Pre-declared
# empty so the ``finally`` can reference them unconditionally even
# on an early raise. Building here (not in ``__init__``) keeps the
# bearer token's in-process lifetime as short as possible.
auth_kwargs: Dict[str, Any] = {}
tls_kwargs: Dict[str, Any] = {}
try:
# ``session_conf`` is forwarded to the kernel verbatim. Unlike the
# Thrift backend (``thrift_backend.py``), the kernel/SEA path does
# NOT inject or validate ``timestampAsString``: the kernel owns type
# handling natively (Arrow / complex-types), and that flag is a
# Thrift-only ``ExecuteStatement`` conf that has no meaning on the
# SEA session path, so there is nothing to pin or guard here.
auth_kwargs = kernel_auth_kwargs(
self._auth_provider,
self._auth_options,
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/test_kernel_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,37 @@ def test_open_session_rejects_double_open(monkeypatch):
c.open_session(session_configuration=None, catalog=None, schema=None)


def test_open_session_forwards_timestamp_as_string_verbatim(monkeypatch):
"""The kernel/SEA path does NOT guard the Thrift-only
``timestampAsString`` conf: it is forwarded to the kernel verbatim
(stringified) rather than rejected, since that flag has no meaning
on the SEA session path and nobody passes it on purpose."""
captured = {}

def fake_session(**kw):
captured.update(kw)
sess = MagicMock()
sess.session_id = "sess-id"
return sess

monkeypatch.setattr(kernel_client._kernel, "Session", fake_session)

c = _make_client()

c.open_session(
session_configuration={
"spark.thriftserver.arrowBasedRowSet.timestampAsString": True
},
catalog=None,
schema=None,
)

assert (
captured["session_conf"]["spark.thriftserver.arrowBasedRowSet.timestampAsString"]
== "True"
)


@pytest.mark.parametrize(
"kwargs, expected_flag",
[
Expand Down
Loading