Skip to content

Commit e2e33b4

Browse files
feat: add oauth_token_cache_enabled to control kernel U2M on-disk token cache
Adds the oauth_token_cache_enabled connect() kwarg, forwarded to the pyo3 Session token_cache_enabled field on the oauth-u2m path. U2M-only; disabled by default (matches Thrift no-persistence posture); enable-flag only; experimental_oauth_persistence is untouched. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
1 parent c77f275 commit e2e33b4

5 files changed

Lines changed: 104 additions & 0 deletions

File tree

CONNECTION_PARAMETERS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ to change without notice.
8282
| `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. |
8383
| `identity_federation_client_id` | `str` ||| `None` | Workload identity / token-federation client id (kernel support added in #910). |
8484
| `experimental_oauth_persistence` | `OAuthPersistence` ||| `None` | **Thrift-only.** The kernel owns its own token lifecycle and does not accept a persistence store. |
85+
| `oauth_token_cache_enabled` | `bool \| None` ||| `None` | **Kernel-only, U2M-only.** Controls whether the kernel persists OAuth U2M refresh tokens to disk (AES-256 encrypted, at `~/.config/databricks-sql-kernel/oauth/`, requires databricks-sql-kernel PR #283). When unset (None, default), the kernel's default applies; False disables persistence (in-memory only); True enables on-disk cache. Distinct from `experimental_oauth_persistence` — this controls the kernel's built-in encrypted storage, not a pluggable callback. |
8586
| `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. |
8687
| `azure_workspace_resource_id` | `str` ||| `None` | For `azure-sp-m2m`. When set, the SP **management token** (`X-Databricks-Azure-SP-Management-Token`) + `X-Databricks-Azure-Workspace-Resource-Id` header are sent, to authorize an SP that has an Azure RBAC role but is not a workspace member. Omit it for a workspace-member SP (the data token authenticates alone; no management token is fetched). Works on both the kernel and Thrift paths. |
8788
| `_use_cert_as_auth` (+ `_tls_client_cert_file`) | `bool` ||| `False` | Authenticate with a TLS client certificate instead of a token. Thrift-only. |

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,12 @@ def kernel_auth_kwargs(
460460
else list(PYSQL_OAUTH_REDIRECT_PORT_RANGE)
461461
),
462462
"oauth_scopes": scopes if scopes is not None else list(PYSQL_OAUTH_SCOPES),
463+
# OAuth U2M token-cache enable/disable: when present in auth_options,
464+
# forward to the kernel as token_cache_enabled on the U2M branch.
465+
# Default disabled (bool(None) = False) for backward compatibility when
466+
# moving token persistence control to the kernel. This ensures callers
467+
# must opt-in to on-disk persistence rather than silently enabling it.
468+
"token_cache_enabled": bool(opts.get("oauth_token_cache_enabled")),
463469
}
464470
if federation_client_id:
465471
kwargs["identity_federation_client_id"] = federation_client_id

src/databricks/sql/client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,17 @@ def read(self) -> Optional[OAuthToken]:
225225
experimental_oauth_persistence=DevOnlyFilePersistence("~/dev-oauth.json")
226226
)
227227
```
228+
:param oauth_token_cache_enabled: `bool | None`, optional (default is None)
229+
**Kernel-only, U2M-only.** Controls whether the kernel persists OAuth U2M
230+
refresh tokens to disk (AES-256 encrypted, at `~/.config/databricks-sql-kernel/oauth/`).
231+
When unset (None, the default), the kernel's own default behavior applies.
232+
When True, enables persistent on-disk token cache; when False, tokens are
233+
held in memory only and the user must re-authenticate when the process restarts.
234+
Has no effect on Thrift or SEA backends, which maintain their own token
235+
lifecycle via `experimental_oauth_persistence`. This parameter is distinct
236+
from the Thrift-only `experimental_oauth_persistence` — this controls the
237+
kernel's built-in encrypted storage, whereas `experimental_oauth_persistence`
238+
is a pluggable callback interface for Thrift-path custom storage.
228239
:param _use_arrow_native_complex_types: `bool`, optional
229240
Controls whether a complex type field value is returned as a string or as a native Arrow type. Defaults to True.
230241
When True:

src/databricks/sql/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ def _create_backend(
186186
"identity_federation_client_id": kwargs.get(
187187
"identity_federation_client_id"
188188
),
189+
# OAuth U2M token-cache enable/disable: controls whether the kernel
190+
# persists U2M refresh tokens to disk (encrypted, at ~/.config/databricks-sql-kernel/oauth/).
191+
# Omitted ⇒ kernel default (enabled); False ⇒ in-memory only.
192+
# This is forwarded to the kernel's pyo3 Session as token_cache_enabled
193+
# on the oauth-u2m auth branch only, ensuring backward compat when moved to the kernel path.
194+
"oauth_token_cache_enabled": kwargs.get("oauth_token_cache_enabled"),
189195
# Azure Entra SP credentials for the azure-sp-m2m path. The
190196
# kernel owns Azure resolution (endpoint/scope/tenant discovery),
191197
# so these raw kwargs are the only source; without threading them

tests/unit/test_kernel_auth_bridge.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ def test_bare_databricks_oauth_forwards_full_python_bundle(self):
432432
# Full registered port list → the kernel binds the first free one.
433433
"redirect_ports": list(PYSQL_OAUTH_REDIRECT_PORT_RANGE),
434434
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
435+
# token_cache_enabled defaults to False (disable-by-default).
436+
"token_cache_enabled": False,
435437
}
436438

437439
def test_azure_oauth_maps_to_in_house_u2m(self):
@@ -450,6 +452,8 @@ def test_azure_oauth_maps_to_in_house_u2m(self):
450452
"client_id": PYSQL_OAUTH_CLIENT_ID,
451453
"redirect_ports": list(PYSQL_OAUTH_REDIRECT_PORT_RANGE),
452454
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
455+
# token_cache_enabled defaults to False (disable-by-default).
456+
"token_cache_enabled": False,
453457
}
454458

455459
def test_azure_oauth_honors_custom_client_id_port_and_scopes(self):
@@ -469,6 +473,8 @@ def test_azure_oauth_honors_custom_client_id_port_and_scopes(self):
469473
"client_id": "custom-client",
470474
"redirect_ports": [9999],
471475
"oauth_scopes": ["custom-scope", "offline_access"],
476+
# token_cache_enabled defaults to False (disable-by-default).
477+
"token_cache_enabled": False,
472478
}
473479

474480
def test_u2m_custom_client_id_port_and_scopes_honored(self):
@@ -489,6 +495,8 @@ def test_u2m_custom_client_id_port_and_scopes_honored(self):
489495
"client_id": "custom-client",
490496
"redirect_ports": [9999],
491497
"oauth_scopes": ["custom-scope", "offline_access"],
498+
# token_cache_enabled defaults to False (disable-by-default).
499+
"token_cache_enabled": False,
492500
}
493501

494502
def test_u2m_custom_client_id_only_falls_back_to_connector_defaults(self):
@@ -507,6 +515,8 @@ def test_u2m_custom_client_id_only_falls_back_to_connector_defaults(self):
507515
"client_id": "custom-client",
508516
"redirect_ports": list(PYSQL_OAUTH_REDIRECT_PORT_RANGE),
509517
"oauth_scopes": list(PYSQL_OAUTH_SCOPES),
518+
# token_cache_enabled defaults to False (disable-by-default).
519+
"token_cache_enabled": False,
510520
}
511521

512522
def test_u2m_redirect_port_coerced_to_int(self):
@@ -578,6 +588,76 @@ def test_u2m_normalizes_space_delimited_scopes(self):
578588
)
579589
assert kwargs["oauth_scopes"] == ["all-apis", "offline_access"]
580590

591+
def test_u2m_token_cache_enabled_unset_defaults_to_false(self):
592+
# When oauth_token_cache_enabled is omitted, the kernel U2M kwargs
593+
# must include token_cache_enabled=False (disable-by-default) so the
594+
# kernel does not silently start persisting tokens to disk.
595+
kwargs = kernel_auth_kwargs(
596+
_FakeOAuthProvider(),
597+
{"auth_type": "databricks-oauth"},
598+
)
599+
assert kwargs["token_cache_enabled"] is False
600+
601+
def test_u2m_token_cache_enabled_false_forwarded(self):
602+
# When oauth_token_cache_enabled=False, forward token_cache_enabled=False.
603+
kwargs = kernel_auth_kwargs(
604+
_FakeOAuthProvider(),
605+
{
606+
"auth_type": "databricks-oauth",
607+
"oauth_token_cache_enabled": False,
608+
},
609+
)
610+
assert kwargs["token_cache_enabled"] is False
611+
612+
def test_u2m_token_cache_enabled_true_forwarded(self):
613+
# When oauth_token_cache_enabled=True, forward token_cache_enabled=True.
614+
kwargs = kernel_auth_kwargs(
615+
_FakeOAuthProvider(),
616+
{
617+
"auth_type": "databricks-oauth",
618+
"oauth_token_cache_enabled": True,
619+
},
620+
)
621+
assert kwargs["token_cache_enabled"] is True
622+
623+
@pytest.mark.parametrize("u2m_auth_type", ["databricks-oauth", "azure-oauth"])
624+
def test_u2m_token_cache_enabled_both_auth_types(self, u2m_auth_type):
625+
# token_cache_enabled applies to both databricks-oauth and azure-oauth U2M types.
626+
kwargs = kernel_auth_kwargs(
627+
_FakeOAuthProvider(),
628+
{
629+
"auth_type": u2m_auth_type,
630+
"oauth_token_cache_enabled": True,
631+
},
632+
)
633+
assert kwargs["token_cache_enabled"] is True
634+
635+
def test_token_cache_enabled_not_forwarded_to_m2m(self):
636+
# oauth_token_cache_enabled should NOT be forwarded on the M2M path
637+
# (M2M handles its own token lifecycle independently).
638+
kwargs = kernel_auth_kwargs(
639+
_FakeOAuthProvider(),
640+
{
641+
"oauth_client_id": "sp-uuid",
642+
"oauth_client_secret": "shh",
643+
"oauth_token_cache_enabled": True,
644+
},
645+
)
646+
# On the M2M path, token_cache_enabled should NOT be present.
647+
assert "token_cache_enabled" not in kwargs
648+
assert kwargs["auth_type"] == "oauth-m2m"
649+
650+
def test_token_cache_enabled_not_forwarded_to_pat(self):
651+
# oauth_token_cache_enabled should NOT be forwarded on the PAT path
652+
# (PAT is a static token with no refresh/cache mechanism).
653+
kwargs = kernel_auth_kwargs(
654+
AccessTokenAuthProvider("dapi-xyz"),
655+
{"oauth_token_cache_enabled": True},
656+
)
657+
# On the PAT path, token_cache_enabled should NOT be present.
658+
assert "token_cache_enabled" not in kwargs
659+
assert kwargs["auth_type"] == "pat"
660+
581661

582662
class TestKernelIdentityFederationClientId:
583663
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)