diff --git a/KERNEL_REV b/KERNEL_REV index 7dd91996..95cfce81 100644 --- a/KERNEL_REV +++ b/KERNEL_REV @@ -1 +1 @@ -0d46716c466897148dfc1d2976ff03bdf097998c +eff8950428f4e6cc9975c663ec919f334962f7d0 diff --git a/lib/contracts/IDBSQLClient.ts b/lib/contracts/IDBSQLClient.ts index bbaa4c69..88bcb980 100644 --- a/lib/contracts/IDBSQLClient.ts +++ b/lib/contracts/IDBSQLClient.ts @@ -14,6 +14,8 @@ type AuthOptions = | { authType?: 'access-token'; token: string; + /** Kernel backend: selects mandatory SP-wide Workload Identity Federation. */ + identityFederationClientId?: string; } | { authType: 'databricks-oauth'; @@ -26,6 +28,8 @@ type AuthOptions = // U2M flow to `['sql', 'offline_access']` (parity with the Thrift driver's // `defaultOAuthScopes`), overriding the kernel's bare `all-apis offline_access`. oauthScopes?: Array; + /** Kernel backend: selects mandatory SP-wide Workload Identity Federation. */ + identityFederationClientId?: string; } | { authType: 'custom'; diff --git a/lib/kernel/KernelAuth.ts b/lib/kernel/KernelAuth.ts index 289c3e47..e84f161b 100644 --- a/lib/kernel/KernelAuth.ts +++ b/lib/kernel/KernelAuth.ts @@ -61,6 +61,9 @@ const DEFAULT_OAUTH_CLIENT_ID = 'databricks-sql-connector'; * everything else (client_id, scopes, callback timeout, * token_url_override) uses kernel defaults. * + * A non-empty `identityFederationClientId` selects mandatory SP-wide + * workload-identity token exchange for every auth mode. + * * The `authMode` string literals MUST match the napi-emitted `AuthMode` * variant names verbatim (`'Pat'`, `'OAuthM2m'`, `'OAuthU2m'` — napi-rs's * `#[napi(string_enum)]` without an explicit case option emits the @@ -212,10 +215,19 @@ export interface KernelProxyOptions { }; } +export interface KernelFederationOptions { + /** + * SP-wide Workload Identity Federation client id. Omitted selects BYOT / + * account-wide WIF. + */ + identityFederationClientId?: string; +} + export type KernelNativeConnectionOptions = KernelSessionDefaults & KernelTlsOptions & KernelHttpOptions & KernelProxyOptions & + KernelFederationOptions & ( | { hostName: string; @@ -559,7 +571,8 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel maxConnections?: number; } & KernelTlsOptions & KernelHttpOptions & - KernelProxyOptions = { + KernelProxyOptions & + KernelFederationOptions = { hostName: options.host, httpPath: prependSlash(options.path), // Match the NodeJS Thrift driver, which surfaces INTERVAL columns as @@ -579,6 +592,11 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel ...buildKernelProxyOptions(options), }; + const { identityFederationClientId } = options as { identityFederationClientId?: string }; + if (identityFederationClientId) { + base.identityFederationClientId = identityFederationClientId; + } + // kernel-only pool sizing; read via cast to match how this function reads the // other kernel-specific options (TLS) — they live on the internal options // surface, not the published public `ConnectionOptions` `.d.ts`. diff --git a/native/kernel/index.d.ts b/native/kernel/index.d.ts index 0b042121..f401c31e 100644 --- a/native/kernel/index.d.ts +++ b/native/kernel/index.d.ts @@ -159,7 +159,7 @@ export interface ProxyInput { * - `Pat` — `token` required. * - `OAuthM2m` — `oauthClientId` + `oauthClientSecret` required. * - `OAuthU2m` — `oauthClientId` / `oauthRedirectPort` optional - * (defaults to the `databricks-sql-connector` client on port 8020). + * (defaults to the `databricks-sql-connector` client on port 8030). * * Catalog / schema / sessionConf are applied once at session creation * and remain in effect for every statement run on the resulting @@ -197,14 +197,19 @@ export interface ConnectionOptions { oauthClientSecret?: string /** * Localhost callback port for the [`AuthMode::OAuthU2m`] browser - * flow. Omitted ⇒ kernel default (8020). + * flow. Omitted ⇒ kernel default (8030). */ oauthRedirectPort?: number /** * OAuth scopes override (M2M / U2M). Omitted ⇒ kernel defaults - * (`["all-apis"]` for M2M; `["all-apis", "offline_access"]` for U2M). + * (`["all-apis"]` for M2M; `["sql", "offline_access"]` for U2M). */ oauthScopes?: Array + /** + * SP-wide Workload Identity Federation client id used during mandatory + * token exchange. Omitted selects BYOT / account-wide WIF. + */ + identityFederationClientId?: string /** * Default catalog for statements executed on this session. * Routed through the kernel's `DefaultOpts` and onto the SEA diff --git a/tests/unit/kernel/auth-m2m.test.ts b/tests/unit/kernel/auth-m2m.test.ts index 7b55bcb2..7df6b6d3 100644 --- a/tests/unit/kernel/auth-m2m.test.ts +++ b/tests/unit/kernel/auth-m2m.test.ts @@ -43,6 +43,19 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { }); }); + it('forwards a federation client id on M2M auth', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + oauthClientId: 'client-uuid', + oauthClientSecret: 'dose-fake-secret', + identityFederationClientId: 'federation-client', + }); + + expect(native.identityFederationClientId).to.equal('federation-client'); + }); + it('defaults M2M oauthScopes to all-apis (Thrift + kernel parity)', () => { const native = buildKernelConnectionOptions({ host: 'example.cloud.databricks.com', @@ -190,6 +203,7 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { authType: 'databricks-oauth', oauthClientId: 'client-uuid', oauthClientSecret: 'dose-fake-secret', + identityFederationClientId: 'federation-client', }); const session = await backend.openSession({}); @@ -207,6 +221,7 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => { oauthClientId: 'client-uuid', oauthClientSecret: 'dose-fake-secret', oauthScopes: ['all-apis'], + identityFederationClientId: 'federation-client', }); await session.close(); diff --git a/tests/unit/kernel/auth-pat.test.ts b/tests/unit/kernel/auth-pat.test.ts index 5304298c..9fac0088 100644 --- a/tests/unit/kernel/auth-pat.test.ts +++ b/tests/unit/kernel/auth-pat.test.ts @@ -53,6 +53,28 @@ describe('KernelAuth — PAT auth options builder', () => { } }); + it('forwards a federation client id on PAT auth', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + token: 'dapi-fake-pat', + identityFederationClientId: 'federation-client', + }); + + expect(native.identityFederationClientId).to.equal('federation-client'); + }); + + it('omits an empty federation client id', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + token: 'dapi-fake-pat', + identityFederationClientId: '', + }); + + expect(native).not.to.have.property('identityFederationClientId'); + }); + it('prepends `/` to a path missing the leading slash', () => { const opts: ConnectionOptions = { host: 'example.cloud.databricks.com', diff --git a/tests/unit/kernel/auth-u2m.test.ts b/tests/unit/kernel/auth-u2m.test.ts index c21493d5..3ca8b4cc 100644 --- a/tests/unit/kernel/auth-u2m.test.ts +++ b/tests/unit/kernel/auth-u2m.test.ts @@ -40,6 +40,17 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => { }); }); + it('forwards a federation client id on U2M auth', () => { + const native = buildKernelConnectionOptions({ + host: 'example.cloud.databricks.com', + path: '/sql/1.0/warehouses/abc', + authType: 'databricks-oauth', + identityFederationClientId: 'federation-client', + }); + + expect(native.identityFederationClientId).to.equal('federation-client'); + }); + it('defaults U2M oauthScopes to Thrift parity (sql offline_access)', () => { const native = buildKernelConnectionOptions({ host: 'example.cloud.databricks.com',