Skip to content

Commit 8f4daee

Browse files
authored
feat(kernel): forward identity federation client ID (#910)
* feat(kernel): forward identity federation client ID * docs: preserve existing kernel comment * test(kernel): cover federation client ID forwarding
1 parent 5574e01 commit 8f4daee

7 files changed

Lines changed: 72 additions & 10 deletions

File tree

KERNEL_REV

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7ffb30d533c08651ca707b8dd13894c9e01cb68e
1+
eff8950428f4e6cc9975c663ec919f334962f7d0

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ Notes:
5252
`cp310-abi3`). On older interpreters the `[kernel]` extra installs
5353
nothing and `use_kernel=True` raises an `ImportError`.
5454
- The extra also pulls in PyArrow, which the kernel result path requires.
55-
- Authentication supports PAT (`access_token`), OAuth M2M, and OAuth U2M.
55+
- Authentication supports PAT (`access_token`), OAuth M2M/U2M, and SP-wide
56+
workload identity federation (`identity_federation_client_id`).
5657

5758

5859
```bash

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
kernel's ``auth_type='oauth-u2m'`` and the kernel runs the browser
2222
flow itself.
2323
24+
``identity_federation_client_id`` is forwarded with whichever auth shape
25+
wins resolution. It selects mandatory SP-wide workload-identity token
26+
exchange in the kernel; omitting it preserves BYOT / account-wide behavior.
27+
2428
A user-supplied custom ``credentials_provider`` is **rejected** on the
2529
kernel path with ``NotSupportedError``: it's an opaque token source
2630
with no extractable raw credentials, so the kernel can't own the
@@ -125,10 +129,10 @@ def kernel_auth_kwargs(
125129
126130
``auth_options`` carries the raw connect() kwargs relevant to auth
127131
(``auth_type``, ``oauth_client_id``, ``oauth_client_secret``,
128-
``oauth_redirect_port``, ``credentials_provider``). They drive the
129-
OAuth decisions because the OAuth secret is consumed during
130-
``AuthProvider`` construction and can't be read back off the built
131-
provider.
132+
``oauth_redirect_port``, ``credentials_provider``,
133+
``identity_federation_client_id``). They drive the OAuth decisions
134+
because the OAuth secret is consumed during ``AuthProvider``
135+
construction and can't be read back off the built provider.
132136
133137
Resolution order:
134138
@@ -161,6 +165,7 @@ def kernel_auth_kwargs(
161165

162166
client_id = opts.get("oauth_client_id")
163167
client_secret = opts.get("oauth_client_secret")
168+
federation_client_id = opts.get("identity_federation_client_id")
164169
auth_type = opts.get("auth_type")
165170
has_m2m = bool(client_id and client_secret)
166171

@@ -191,6 +196,8 @@ def kernel_auth_kwargs(
191196
scopes = _normalize_scopes(opts.get("oauth_scopes"))
192197
if scopes is not None:
193198
kwargs["oauth_scopes"] = scopes
199+
if federation_client_id:
200+
kwargs["identity_federation_client_id"] = federation_client_id
194201
return kwargs
195202

196203
# 2. PAT (including TokenFederationProvider-wrapped PAT).
@@ -201,7 +208,10 @@ def kernel_auth_kwargs(
201208
"PAT auth provider did not produce a Bearer Authorization "
202209
"header; cannot route through the kernel's PAT path"
203210
)
204-
return {"auth_type": "pat", "access_token": token}
211+
kwargs = {"auth_type": "pat", "access_token": token}
212+
if federation_client_id:
213+
kwargs["identity_federation_client_id"] = federation_client_id
214+
return kwargs
205215

206216
# 3. OAuth U2M — browser authorization-code flow; the kernel runs it.
207217
if auth_type in ("databricks-oauth", "azure-oauth"):
@@ -214,6 +224,8 @@ def kernel_auth_kwargs(
214224
scopes = _normalize_scopes(opts.get("oauth_scopes"))
215225
if scopes is not None:
216226
kwargs["oauth_scopes"] = scopes
227+
if federation_client_id:
228+
kwargs["identity_federation_client_id"] = federation_client_id
217229
return kwargs
218230

219231
# 4. Custom credentials_provider — the connector's primary M2M path

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ def __init__(
206206
# Forwarded to the kernel Session in ``open_session``.
207207
self._http_headers = http_headers or []
208208
# Raw auth-relevant connect() kwargs (auth_type,
209-
# oauth_client_id/secret, redirect port, credentials_provider).
209+
# oauth_client_id/secret, redirect port, credentials_provider,
210+
# identity_federation_client_id).
210211
# The kernel auth bridge needs these to build OAuth kwargs — the
211212
# OAuth secret is consumed during ``auth_provider`` construction
212213
# and isn't recoverable from the built provider.

src/databricks/sql/client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ def __init__(
170170
port of the oauth redirect uri (localhost). This is required when custom oauth client_id
171171
`oauth_client_id` is set
172172
173+
identity_federation_client_id: `str`, optional
174+
Service-principal client ID for mandatory SP-wide workload identity
175+
token exchange. Supported by both the default and kernel backends.
176+
173177
user_agent_entry: `str`, optional
174178
A custom tag to append to the User-Agent header. This is typically used by partners to identify their applications.. If not specified, it will use the default user agent PyDatabricksSqlConnector
175179

src/databricks/sql/session.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,19 @@ def _create_backend(
164164
# original credentials. On this path we intentionally did
165165
# NOT build the connector's own OAuth provider (see __init__
166166
# above), so these raw kwargs are the only source of the
167-
# OAuth client id/secret. These are kernel-only; the Thrift
168-
# / SEA backends are unaffected.
167+
# OAuth client id/secret and optional federation client id.
168+
# These are kernel-only; the Thrift / SEA backends are
169+
# unaffected.
169170
kernel_auth_options = {
170171
"auth_type": kwargs.get("auth_type"),
171172
"oauth_client_id": kwargs.get("oauth_client_id"),
172173
"oauth_client_secret": kwargs.get("oauth_client_secret"),
173174
"oauth_redirect_port": kwargs.get("oauth_redirect_port"),
174175
"oauth_scopes": kwargs.get("oauth_scopes"),
175176
"credentials_provider": kwargs.get("credentials_provider"),
177+
"identity_federation_client_id": kwargs.get(
178+
"identity_federation_client_id"
179+
),
176180
}
177181
# Forward the connector's retry-tuning kwargs so the kernel's
178182
# own retry policy honours them (the kernel owns the retry

tests/unit/test_kernel_auth_bridge.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,46 @@ def test_u2m_forwards_scopes(self, auth_type):
272272
assert kwargs["oauth_scopes"] == ["all-apis", "offline_access"]
273273

274274

275+
class TestKernelIdentityFederationClientId:
276+
@pytest.mark.parametrize(
277+
"auth_provider,auth_options",
278+
[
279+
pytest.param(AccessTokenAuthProvider("dapi-xyz"), {}, id="pat"),
280+
pytest.param(
281+
_FakeOAuthProvider(),
282+
{"oauth_client_id": "sp-uuid", "oauth_client_secret": "shh"},
283+
id="m2m",
284+
),
285+
pytest.param(
286+
_FakeOAuthProvider(),
287+
{"auth_type": "databricks-oauth"},
288+
id="u2m",
289+
),
290+
],
291+
)
292+
@pytest.mark.parametrize(
293+
"federation_client_id",
294+
[
295+
pytest.param(None, id="omitted"),
296+
pytest.param("", id="empty"),
297+
pytest.param("federation-client", id="supplied"),
298+
],
299+
)
300+
def test_forwards_only_non_empty_value(
301+
self, auth_provider, auth_options, federation_client_id
302+
):
303+
options = dict(auth_options)
304+
if federation_client_id is not None:
305+
options["identity_federation_client_id"] = federation_client_id
306+
307+
kwargs = kernel_auth_kwargs(auth_provider, options)
308+
309+
if federation_client_id:
310+
assert kwargs["identity_federation_client_id"] == federation_client_id
311+
else:
312+
assert "identity_federation_client_id" not in kwargs
313+
314+
275315
class TestKernelAuthAmbiguity:
276316
"""Conflicting auth signals must fail loudly at session-open rather
277317
than silently resolving to one flow (which would surface later as a

0 commit comments

Comments
 (0)