Skip to content

Commit d6c0d81

Browse files
committed
fix(kernel): request Thrift-parity OAuth scopes, configurable via oauthScopes
The kernel U2M flow passed no scopes, so it fell through to the kernel's bare default (all-apis offline_access). The databricks-sql-connector OAuth app is registered for `sql`, so U2M auth used the wrong scope set. Pass scopes explicitly from the driver: - U2M defaults to ['sql', 'offline_access'] (matches the Thrift driver's defaultOAuthScopes), overriding the kernel's all-apis default. - M2M defaults to ['all-apis'] (matches Thrift + the kernel's M2M default). - Both overridable via a new `oauthScopes` connect option — closing the configurability gap with pyo3, which already forwards `scopes` on M2M. Driver-only change: the napi binding already forwards oauth_scopes and the kernel's u2m.rs/m2m.rs feed them into the authorize/token request. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
1 parent baa55ac commit d6c0d81

4 files changed

Lines changed: 94 additions & 0 deletions

File tree

lib/contracts/IDBSQLClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ type AuthOptions =
2222
oauthClientId?: string;
2323
oauthClientSecret?: string;
2424
useDatabricksOAuthInAzure?: boolean;
25+
// OAuth scopes to request. When omitted, the kernel backend defaults the
26+
// U2M flow to `['sql', 'offline_access']` (parity with the Thrift driver's
27+
// `defaultOAuthScopes`), overriding the kernel's bare `all-apis offline_access`.
28+
oauthScopes?: Array<string>;
2529
}
2630
| {
2731
authType: 'custom';

lib/kernel/KernelAuth.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ import { buildUserAgentString } from '../utils';
2828
*/
2929
const U2M_DEFAULT_REDIRECT_PORT = 8030;
3030

31+
// U2M OAuth scopes default. Matches the standalone Thrift driver's
32+
// `defaultOAuthScopes` (lib/connection/auth/DatabricksOAuth/OAuthScope.ts):
33+
// `['sql', 'offline_access']`. The kernel's bare default is
34+
// `['all-apis', 'offline_access']`; the `databricks-sql-connector` OAuth app is
35+
// registered for the `sql` scope, so we pass the Thrift-parity scopes explicitly
36+
// unless the caller overrides via `oauthScopes`.
37+
const U2M_DEFAULT_SCOPES = ['sql', 'offline_access'];
38+
39+
// M2M OAuth scopes default. Matches the standalone Thrift driver (`getScopes`
40+
// forces `['all-apis']` for the client-credentials flow) and the kernel's own
41+
// M2M default (`m2m.rs` → `['all-apis']`). Overridable via `oauthScopes`
42+
// (parity with pyo3, which forwards `scopes` on M2M).
43+
const M2M_DEFAULT_SCOPES = ['all-apis'];
44+
3145
/**
3246
* Shape consumed by the napi-binding's `openSession()` (see
3347
* `native/kernel/index.d.ts`). Mirrors `ConnectionOptions` in the binding's
@@ -189,12 +203,14 @@ export type KernelNativeConnectionOptions = KernelSessionDefaults &
189203
authMode: 'OAuthM2m';
190204
oauthClientId: string;
191205
oauthClientSecret: string;
206+
oauthScopes?: Array<string>;
192207
}
193208
| {
194209
hostName: string;
195210
httpPath: string;
196211
authMode: 'OAuthU2m';
197212
oauthRedirectPort: number;
213+
oauthScopes?: Array<string>;
198214
}
199215
);
200216

@@ -541,6 +557,7 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
541557
const oauth = options as {
542558
oauthClientId?: string;
543559
oauthClientSecret?: string;
560+
oauthScopes?: Array<string>;
544561
azureTenantId?: string;
545562
useDatabricksOAuthInAzure?: boolean;
546563
persistence?: unknown;
@@ -627,6 +644,13 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
627644
...base,
628645
authMode: 'OAuthU2m',
629646
oauthRedirectPort: U2M_DEFAULT_REDIRECT_PORT,
647+
// Pass scopes explicitly so the kernel requests the same set as the
648+
// Thrift driver (`sql offline_access`) rather than its bare-Rust
649+
// `all-apis offline_access` default. Caller can override via `oauthScopes`.
650+
oauthScopes:
651+
Array.isArray(oauth.oauthScopes) && oauth.oauthScopes.length > 0
652+
? oauth.oauthScopes
653+
: U2M_DEFAULT_SCOPES,
630654
};
631655
}
632656

@@ -652,6 +676,12 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
652676
authMode: 'OAuthM2m',
653677
oauthClientId: oauth.oauthClientId,
654678
oauthClientSecret: oauth.oauthClientSecret,
679+
// Configurable (parity with pyo3); defaults to `['all-apis']` — the only
680+
// scope the client-credentials flow allows, matching Thrift + the kernel.
681+
oauthScopes:
682+
Array.isArray(oauth.oauthScopes) && oauth.oauthScopes.length > 0
683+
? oauth.oauthScopes
684+
: M2M_DEFAULT_SCOPES,
655685
};
656686
}
657687

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,34 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => {
4040
authMode: 'OAuthM2m',
4141
oauthClientId: 'client-uuid',
4242
oauthClientSecret: 'dose-fake-secret',
43+
oauthScopes: ['all-apis'],
4344
});
4445
});
4546

47+
it('defaults M2M oauthScopes to all-apis (Thrift + kernel parity)', () => {
48+
const native = buildKernelConnectionOptions({
49+
host: 'example.cloud.databricks.com',
50+
path: '/sql/1.0/warehouses/abc',
51+
authType: 'databricks-oauth',
52+
oauthClientId: 'client-uuid',
53+
oauthClientSecret: 'dose-fake-secret',
54+
});
55+
expect(native.authMode).to.equal('OAuthM2m');
56+
expect((native as { oauthScopes?: string[] }).oauthScopes).to.deep.equal(['all-apis']);
57+
});
58+
59+
it('honors a caller-supplied M2M oauthScopes override (parity with pyo3)', () => {
60+
const native = buildKernelConnectionOptions({
61+
host: 'example.cloud.databricks.com',
62+
path: '/sql/1.0/warehouses/abc',
63+
authType: 'databricks-oauth',
64+
oauthClientId: 'client-uuid',
65+
oauthClientSecret: 'dose-fake-secret',
66+
oauthScopes: ['sql', 'offline_access'],
67+
} as ConnectionOptions);
68+
expect((native as { oauthScopes?: string[] }).oauthScopes).to.deep.equal(['sql', 'offline_access']);
69+
});
70+
4671
it('prepends `/` to the path on the M2M branch too', () => {
4772
const opts: ConnectionOptions = {
4873
host: 'example.cloud.databricks.com',
@@ -177,6 +202,7 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => {
177202
authMode: 'OAuthM2m',
178203
oauthClientId: 'client-uuid',
179204
oauthClientSecret: 'dose-fake-secret',
205+
oauthScopes: ['all-apis'],
180206
});
181207

182208
await session.close();

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,42 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => {
3737
intervalsAsString: true,
3838
authMode: 'OAuthU2m',
3939
oauthRedirectPort: 8030,
40+
oauthScopes: ['sql', 'offline_access'],
4041
});
4142
});
4243

44+
it('defaults U2M oauthScopes to Thrift parity (sql offline_access)', () => {
45+
const native = buildKernelConnectionOptions({
46+
host: 'example.cloud.databricks.com',
47+
path: '/sql/1.0/warehouses/abc',
48+
authType: 'databricks-oauth',
49+
});
50+
expect(native.authMode).to.equal('OAuthU2m');
51+
// Matches the standalone Thrift driver's defaultOAuthScopes, NOT the
52+
// kernel's bare `all-apis offline_access` default.
53+
expect((native as { oauthScopes?: string[] }).oauthScopes).to.deep.equal(['sql', 'offline_access']);
54+
});
55+
56+
it('honors a caller-supplied U2M oauthScopes override', () => {
57+
const native = buildKernelConnectionOptions({
58+
host: 'example.cloud.databricks.com',
59+
path: '/sql/1.0/warehouses/abc',
60+
authType: 'databricks-oauth',
61+
oauthScopes: ['all-apis'],
62+
} as ConnectionOptions);
63+
expect((native as { oauthScopes?: string[] }).oauthScopes).to.deep.equal(['all-apis']);
64+
});
65+
66+
it('falls back to the default U2M scopes when oauthScopes is an empty array', () => {
67+
const native = buildKernelConnectionOptions({
68+
host: 'example.cloud.databricks.com',
69+
path: '/sql/1.0/warehouses/abc',
70+
authType: 'databricks-oauth',
71+
oauthScopes: [],
72+
} as ConnectionOptions);
73+
expect((native as { oauthScopes?: string[] }).oauthScopes).to.deep.equal(['sql', 'offline_access']);
74+
});
75+
4376
it('rejects oauthClientId without oauthClientSecret as M2M-with-missing-secret', () => {
4477
// Round-4 NF3-2: presence of `oauthClientId` signals M2M intent.
4578
// Routing now keys off the id (the "do I have an id?" signal),
@@ -143,6 +176,7 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => {
143176
intervalsAsString: true,
144177
authMode: 'OAuthU2m',
145178
oauthRedirectPort: 8030,
179+
oauthScopes: ['sql', 'offline_access'],
146180
});
147181

148182
await session.close();

0 commit comments

Comments
 (0)