Skip to content

Commit 3da798b

Browse files
authored
feat(kernel): support static token federation (#501)
* feat(kernel): forward identity federation client ID * feat(kernel): support static token federation * fix(kernel): reject conflicting static token auth * test(kernel): narrow static token conflict coverage * fix(kernel): narrow static token ambiguity guard * fix(kernel): preserve account-wide federation intent * fix(kernel): document mandatory static token federation
1 parent 2406f31 commit 3da798b

8 files changed

Lines changed: 167 additions & 26 deletions

File tree

CONNECTION_PARAMETERS.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ column.
5555

5656
## Authentication
5757

58-
| Option | Type | Thrift | Kernel | Default Value | Note |
59-
| ---------------------------------------------- | -------------------------------------------------------------------------- | :------: | :------: | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
60-
| `authType` — supported on both backends | `'access-token'` \| `'databricks-oauth'` ||| `'access-token'` | The two auth modes both backends accept. `access-token` uses `token` (PAT) and is the default when `authType` is omitted. `databricks-oauth` covers M2M (`oauthClientId` + `oauthClientSecret`; kernel runs OIDC discovery + client-credentials internally) and U2M (browser; no secret — kernel U2M differs slightly, see the OAuth sub-option rows below). |
61-
| `authType` — Thrift-only | `'custom'` \| `'token-provider'` \| `'external-token'` \| `'static-token'` |||| **Thrift-only.** `custom` (`provider: IAuthentication`), `token-provider` (`tokenProvider: ITokenProvider`), `external-token` (`getToken: TokenCallback`), `static-token` (`staticToken`). The kernel throws `unsupported auth mode` for all four — it supports only the two modes above. |
62-
| `oauthScopes` | `Array<string>` ||| U2M `['sql','offline_access']`, M2M `['all-apis']` | **Thrift ignores `oauthScopes`**`createAuthProvider` never threads it into `DatabricksOAuth`, so `authenticate()` always falls back to `defaultOAuthScopes` (`['sql','offline_access']`). Only the kernel honors a custom `oauthScopes`; its defaults happen to match Thrift's fallback. |
63-
| `oauthClientId` (U2M) | `string` ||| napi default `client_id` when absent | The kernel adapter (`buildKernelConnectionOptions`) forwards a custom `oauthClientId` verbatim on the U2M arm; when it is absent the napi binding applies its own default `client_id`. Whether the native binding then honors or rejects a custom id is not observable from this repo — the TypeScript layer neither hardcodes an id nor rejects one. |
64-
| `oauthClientId` + no secret | `string` | ✅ (U2M) | ✅ (U2M) || **Parity.** The kernel keys flow selection off `oauthClientSecret` presence exactly like Thrift, so `oauthClientId` + no secret routes to **U2M** (with the id forwarded) — it does **not** throw an M2M "secret required" error. |
65-
| `azureTenantId` / `useDatabricksOAuthInAzure` | `string` / `boolean` |||| **Thrift-only.** Kernel rejects Azure-direct (Entra) OAuth; workspace-OIDC discovery covers Azure workspaces without it. |
66-
| `persistence` (custom OAuth token store) | `OAuthPersistence` |||| **Thrift-only.** Kernel throws; it auto-persists U2M tokens to `~/.config/databricks-sql-kernel/oauth/` and does not cache M2M. |
67-
| `enableTokenFederation` / `federationClientId` | `boolean` / `string` || | `false` / — | **Thrift-only** (available on the token-provider / external-token / static-token arms, none of which the kernel supports). |
58+
| Option | Type | Thrift | Kernel | Default Value | Note |
59+
| ---------------------------------------------- | ------------------------------------------------------------ | :------: | :------: | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60+
| `authType` — supported on both backends | `'access-token'` \| `'databricks-oauth'` \| `'static-token'` ||| `'access-token'` | `access-token` uses `token` (PAT) and is the default when `authType` is omitted. `static-token` uses `staticToken`; the kernel maps it to its native bearer-token mode. `databricks-oauth` covers M2M (`oauthClientId` + `oauthClientSecret`) and U2M (browser; no secret). |
61+
| `authType` — Thrift-only | `'custom'` \| `'token-provider'` \| `'external-token'` |||| **Thrift-only.** `custom` uses `provider: IAuthentication`, `token-provider` uses `tokenProvider: ITokenProvider`, and `external-token` uses `getToken: TokenCallback`. The kernel throws `unsupported auth mode` for these modes. |
62+
| `oauthScopes` | `Array<string>` ||| U2M `['sql','offline_access']`, M2M `['all-apis']` | **Thrift ignores `oauthScopes`**`createAuthProvider` never threads it into `DatabricksOAuth`, so `authenticate()` always falls back to `defaultOAuthScopes` (`['sql','offline_access']`). Only the kernel honors a custom `oauthScopes`; its defaults happen to match Thrift's fallback. |
63+
| `oauthClientId` (U2M) | `string` ||| napi default `client_id` when absent | The kernel adapter (`buildKernelConnectionOptions`) forwards a custom `oauthClientId` verbatim on the U2M arm; when it is absent the napi binding applies its own default `client_id`. Whether the native binding then honors or rejects a custom id is not observable from this repo — the TypeScript layer neither hardcodes an id nor rejects one. |
64+
| `oauthClientId` + no secret | `string` | ✅ (U2M) | ✅ (U2M) || **Parity.** The kernel keys flow selection off `oauthClientSecret` presence exactly like Thrift, so `oauthClientId` + no secret routes to **U2M** (with the id forwarded) — it does **not** throw an M2M "secret required" error. |
65+
| `azureTenantId` / `useDatabricksOAuthInAzure` | `string` / `boolean` |||| **Thrift-only.** Kernel rejects Azure-direct (Entra) OAuth; workspace-OIDC discovery covers Azure workspaces without it. |
66+
| `persistence` (custom OAuth token store) | `OAuthPersistence` |||| **Thrift-only.** Kernel throws; it auto-persists U2M tokens to `~/.config/databricks-sql-kernel/oauth/` and does not cache M2M. |
67+
| `enableTokenFederation` / `federationClientId` | `boolean` / `string` || ⚠️ | `false` / — | On the kernel backend these options apply only to `static-token`. Federation is always enabled, so `enableTokenFederation` is ignored; an omitted or empty client ID selects account-wide WIF and a non-empty ID selects SP-wide WIF. Thrift honors the boolean and also supports these options for `token-provider` and `external-token`. |
6868

6969
## HTTP client, proxy, retries
7070

@@ -161,8 +161,7 @@ backend, so they are read regardless of `useKernel`. Defaults are sourced from
161161
1. `enableMetricViewMetadata` — auto-injected for both backends in
162162
`DBSQLClient.openSession`, but the conf key is likely dropped by the
163163
kernel's session-conf allowlist, so it has no effect on the kernel path.
164-
2. Auth types `custom`, `token-provider`, `external-token`, `static-token`,
165-
plus `enableTokenFederation` / `federationClientId`.
164+
2. Auth types `custom`, `token-provider`, and `external-token`.
166165
3. `azureTenantId` / `useDatabricksOAuthInAzure` (Azure-direct OAuth).
167166
4. `persistence` (custom OAuth token store).
168167
5. SOCKS proxies.

KERNEL_REV

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

lib/contracts/IDBSQLClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ type AuthOptions =
4646
| {
4747
authType: 'static-token';
4848
staticToken: string;
49+
/** Ignored by the kernel backend, where token federation is always enabled. */
4950
enableTokenFederation?: boolean;
51+
/** Selects SP-wide federation; omitted selects account-wide federation. */
5052
federationClientId?: string;
5153
};
5254

lib/kernel/KernelAuth.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ const DEFAULT_OAUTH_CLIENT_ID = 'databricks-sql-connector';
6161
* everything else (client_id, scopes, callback timeout,
6262
* token_url_override) uses kernel defaults.
6363
*
64+
* `static-token` reuses the native PAT bearer-token mode, where federation is
65+
* always enabled. `enableTokenFederation` is ignored; a non-empty
66+
* `federationClientId` selects SP-wide WIF and omission selects account-wide.
67+
*
6468
* The `authMode` string literals MUST match the napi-emitted `AuthMode`
6569
* variant names verbatim (`'Pat'`, `'OAuthM2m'`, `'OAuthU2m'` — napi-rs's
6670
* `#[napi(string_enum)]` without an explicit case option emits the
@@ -212,10 +216,19 @@ export interface KernelProxyOptions {
212216
};
213217
}
214218

219+
export interface KernelFederationOptions {
220+
/**
221+
* SP-wide Workload Identity Federation client id. Omitted selects BYOT /
222+
* account-wide WIF.
223+
*/
224+
identityFederationClientId?: string;
225+
}
226+
215227
export type KernelNativeConnectionOptions = KernelSessionDefaults &
216228
KernelTlsOptions &
217229
KernelHttpOptions &
218230
KernelProxyOptions &
231+
KernelFederationOptions &
219232
(
220233
| {
221234
hostName: string;
@@ -443,6 +456,10 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp
443456
* - PAT: `authType: 'access-token'` (or undefined, which already means
444457
* PAT throughout the existing driver — see
445458
* `DBSQLClient.createAuthProvider`).
459+
* - Static token: `authType: 'static-token'` + `staticToken`. The token is
460+
* forwarded through the native PAT bearer-token mode, where federation is
461+
* always enabled. `federationClientId` selects SP-wide WIF; omission
462+
* selects account-wide WIF. `enableTokenFederation` is ignored.
446463
* - OAuth M2M: `authType: 'databricks-oauth'` + `oauthClientId` +
447464
* `oauthClientSecret`. Kernel handles OIDC discovery, client_credentials
448465
* exchange, and re-auth on expiry internally.
@@ -556,7 +573,8 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
556573
maxConnections?: number;
557574
} & KernelTlsOptions &
558575
KernelHttpOptions &
559-
KernelProxyOptions = {
576+
KernelProxyOptions &
577+
KernelFederationOptions = {
560578
hostName: options.host,
561579
httpPath: prependSlash(options.path),
562580
// Match the NodeJS Thrift driver, which surfaces INTERVAL columns as
@@ -621,6 +639,26 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
621639
return { ...base, authMode: 'Pat', token };
622640
}
623641

642+
if (authType === 'static-token') {
643+
const { staticToken, federationClientId } = options as {
644+
staticToken?: string;
645+
federationClientId?: string;
646+
};
647+
if (typeof staticToken !== 'string' || isBlankOrReserved(staticToken)) {
648+
throw new AuthenticationError(
649+
"kernel backend: a non-empty token must be supplied via `staticToken` when using `authType: 'static-token'`.",
650+
);
651+
}
652+
if (oauth.oauthClientId !== undefined || oauth.oauthClientSecret !== undefined) {
653+
throw new HiveDriverError(
654+
'kernel backend: cannot supply `staticToken` alongside `oauthClientId`/`oauthClientSecret` ' +
655+
'on the same connection. Pick one auth mode.',
656+
);
657+
}
658+
base.identityFederationClientId = federationClientId || undefined;
659+
return { ...base, authMode: 'Pat', token: staticToken };
660+
}
661+
624662
if (authType === 'databricks-oauth') {
625663
if ((options as { token?: string }).token !== undefined) {
626664
throw new HiveDriverError(
@@ -694,7 +732,7 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
694732

695733
throw new HiveDriverError(
696734
`kernel backend: unsupported auth mode '${authType}'. ` +
697-
"Supported modes on the kernel backend today: 'access-token' (PAT) and 'databricks-oauth' " +
735+
"Supported modes on the kernel backend today: 'access-token' (PAT), 'static-token', and 'databricks-oauth' " +
698736
'(M2M with oauthClientId+oauthClientSecret, or U2M with neither).',
699737
);
700738
}

lib/kernel/KernelBackend.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ export interface KernelBackendOptions {
4747
* kernel-backed implementation of `IBackend`.
4848
*
4949
* **M0 dispatch model:** the napi binding's `openSession()` already
50-
* builds a kernel `Session` from PAT + hostname + httpPath, so there is
50+
* builds a kernel `Session` from auth options + hostname + httpPath, so there is
5151
* no "connect" round-trip before `openSession` — `connect()` only
52-
* captures the `ConnectionOptions` and validates that PAT auth is in
53-
* use. The actual session open happens inside `openSession()`.
52+
* captures and validates the `ConnectionOptions`. The actual session open
53+
* happens inside `openSession()`.
5454
*
5555
* **Auth validation:** delegates to `buildKernelConnectionOptions` from
5656
* `KernelAuth`, which mirrors the existing DBSQLClient validation pattern
@@ -84,9 +84,8 @@ export default class KernelBackend implements IBackend {
8484
}
8585

8686
public async connect(options: ConnectionOptions): Promise<void> {
87-
// Validate PAT auth + capture the napi-binding option shape.
88-
// Any non-PAT mode (or a missing/empty token) throws here, before
89-
// we ever touch the native binding.
87+
// Validate auth + capture the napi-binding option shape before touching
88+
// the native binding.
9089
// Forward the driver's retry config to the kernel, which owns the retry
9190
// loop on the kernel path. This keeps kernel and Thrift governed by one retry
9291
// config (the same `ClientConfig` knobs the Thrift `HttpRetryPolicy` reads),

native/kernel/index.d.ts

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/unit/kernel/auth-pat.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ describe('KernelAuth — PAT auth options builder', () => {
111111
);
112112
});
113113

114-
it('rejects external-token, static-token, and custom auth modes', () => {
115-
const authTypes = ['external-token', 'static-token', 'custom'] as const;
114+
it('rejects external-token and custom auth modes', () => {
115+
const authTypes = ['external-token', 'custom'] as const;
116116
for (const authType of authTypes) {
117117
// eslint-disable-next-line @typescript-eslint/no-explicit-any
118118
const opts = {

0 commit comments

Comments
 (0)