Skip to content

Commit 64307cd

Browse files
feat(kernel): thread Azure Entra auth (U2M + SP M2M) through the kernel path
On useKernel=true + authType='databricks-oauth', route Azure Entra auth to the kernel instead of rejecting it. Mirrors the Thrift OAuthManager.getManager: useDatabricksOAuthInAzure selects the flavour on an Azure host. - useDatabricksOAuthInAzure: true -> in-house workspace-federated flow, which the kernel runs natively: no secret -> OAuthU2m (browser), secret -> OAuthM2m (workspace-OIDC client-credentials). Works against Azure workspaces. - absent/false on an Azure host -> Entra-direct: with a secret -> the kernel's Azure SP M2M (AzureSpM2m; Entra SP creds ride oauthClientId/oauthClientSecret, azureTenantId optional/auto-discovered); without a secret -> Entra-direct browser U2M, which the kernel does not implement -> rejected with a pointer to useDatabricksOAuthInAzure: true or the Thrift backend. - Non-Azure host: the flags are inert (in-house is the only flow). Adds an AzureSpM2m variant to the native-options union + an isAzureHost helper. Replaces the previous blanket Azure-OAuth rejection. Verified end-to-end against a live Azure workspace (U2M browser flow + SP M2M data token) with a native module built from databricks-sql-kernel#280. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
1 parent 664deee commit 64307cd

4 files changed

Lines changed: 160 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release History
22

3+
## Unreleased
4+
5+
- Kernel backend (`useKernel: true`): **Azure Entra (Azure AD) auth is now threaded through the kernel path.** On `authType: 'databricks-oauth'`, `useDatabricksOAuthInAzure` selects the flavour on an Azure host (mirroring the Thrift `OAuthManager.getManager`): `true` → the in-house workspace-federated flow, which the kernel runs natively (browser U2M → `OAuthU2m`, client-credentials M2M → `OAuthM2m`, both via workspace-OIDC discovery, which works against Azure workspaces); absent/`false` on an Azure host → the Entra-direct flow — with a secret it maps to the kernel's Azure service-principal M2M (`AzureSpM2m`, the Entra SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId` optional and auto-discovered when omitted), and without a secret (Entra-direct browser U2M, which the kernel does not implement) it is rejected with a pointer to `useDatabricksOAuthInAzure: true` or the Thrift backend. On a non-Azure host these flags are inert. Requires a `databricks-sql-kernel` native module that exposes the Azure SP surface ([databricks-sql-kernel#280](https://github.com/databricks/databricks-sql-kernel/pull/280)). (PECOBLR-4141 / PECOBLR-4120)
6+
37
## 2.0.0
48

59
**Breaking changes — completes the security cleanup that 1.17.0 could not do without breaking changes.**

lib/kernel/KernelAuth.ts

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@ export type KernelNativeConnectionOptions = KernelSessionDefaults &
239239
oauthScopes?: Array<string>;
240240
oauthClientId?: string;
241241
}
242+
| {
243+
hostName: string;
244+
httpPath: string;
245+
authMode: 'AzureSpM2m';
246+
azureClientId: string;
247+
azureClientSecret: string;
248+
azureTenantId?: string;
249+
}
242250
);
243251

244252
function prependSlash(str: string): string {
@@ -248,6 +256,28 @@ function prependSlash(str: string): string {
248256
return str;
249257
}
250258

259+
/**
260+
* Azure Databricks host suffixes — the superset the Thrift driver's
261+
* `OAuthManager.getManager` recognises (`.azuredatabricks.net`,
262+
* `.databricks.azure.us`, `.databricks.azure.cn`). Used to decide whether an
263+
* OAuth connection is on Azure and therefore subject to the in-house-vs-
264+
* Entra-direct split.
265+
*/
266+
const AZURE_HOST_SUFFIXES = ['.azuredatabricks.net', '.databricks.azure.us', '.databricks.azure.cn'];
267+
268+
/**
269+
* True when `host` is an Azure Databricks workspace host. Normalises the input
270+
* the same way `getManager` does (lowercase, strip scheme + any path) so a
271+
* caller passing a bare host or a full URL is treated identically.
272+
*/
273+
function isAzureHost(host: string): boolean {
274+
const normalized = host
275+
.toLowerCase()
276+
.replace(/^https?:\/\//, '')
277+
.split('/')[0];
278+
return AZURE_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
279+
}
280+
251281
/**
252282
* Reject inputs that pass `typeof === 'string' && length > 0` but are
253283
* structurally useless as credentials: whitespace-only strings, and the
@@ -467,11 +497,24 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp
467497
* on the U2M arm. Thrift U2M honours a custom `clientId`.
468498
* Both are documented limitations of the M0 kernel OAuth surface, not bugs.
469499
*
500+
* Azure (Entra) on the OAuth path — mirrors Thrift `OAuthManager.getManager`,
501+
* with `useDatabricksOAuthInAzure` selecting the flavour on an Azure host:
502+
* - `useDatabricksOAuthInAzure: true` → **in-house** (workspace-federated).
503+
* The kernel runs it natively via workspace-OIDC discovery — U2M browser
504+
* flow (`OAuthU2m`) and M2M client-credentials (`OAuthM2m`) — so it is NOT
505+
* rejected. `azureTenantId` is ignored here (the in-house flow does not use
506+
* it), matching Thrift.
507+
* - absent/`false` on an Azure host → **Entra-direct**:
508+
* - with a secret → Azure service-principal M2M (`AzureSpM2m`); the Entra
509+
* SP creds ride `oauthClientId`/`oauthClientSecret`, `azureTenantId`
510+
* optional (kernel auto-discovers).
511+
* - without a secret → Entra-direct browser U2M, which the kernel does
512+
* not implement → **rejected** with a pointer to
513+
* `useDatabricksOAuthInAzure: true` or the Thrift backend.
514+
* - On a non-Azure host these flags are inert (the in-house flow is the only
515+
* one), matching Thrift.
516+
*
470517
* Out of scope on the OAuth paths (rejected with a clear error):
471-
* - `azureTenantId` / `useDatabricksOAuthInAzure` → Microsoft Entra
472-
* direct flow. The kernel uses workspace-OIDC discovery (which works
473-
* against Azure workspaces too — they serve `/oidc/.well-known/...`)
474-
* and does not implement the Entra-direct scope-rewrite path.
475518
* - `persistence` on M2M → M2M tokens are not cached (re-issuing is
476519
* cheap; no refresh token).
477520
* - `persistence` on U2M → custom token store is a parity gap;
@@ -485,7 +528,7 @@ export function buildKernelHttpOptions(options: ConnectionOptions): KernelHttpOp
485528
*
486529
* Throws:
487530
* - `AuthenticationError` for missing/blank required credentials.
488-
* - `HiveDriverError` for unsupported auth modes / Azure-direct /
531+
* - `HiveDriverError` for unsupported auth modes / Entra-direct U2M /
489532
* custom persistence / ambiguous combinations.
490533
*/
491534
/**
@@ -632,12 +675,45 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
632675
);
633676
}
634677

635-
if (oauth.azureTenantId !== undefined || oauth.useDatabricksOAuthInAzure === true) {
636-
throw new HiveDriverError(
637-
'kernel backend: Azure-direct OAuth (azureTenantId / useDatabricksOAuthInAzure) ' +
638-
'is not supported. The workspace-OIDC discovery path handles Azure workspaces ' +
639-
'today without these options.',
640-
);
678+
// Azure routing. `useDatabricksOAuthInAzure` selects the in-house
679+
// (workspace-federated) flow vs the Entra-direct flow, mirroring the Thrift
680+
// driver's `OAuthManager.getManager`: on an Azure host, `true` → in-house,
681+
// absent/false → Entra-direct. The kernel runs the in-house flow natively
682+
// via workspace-OIDC discovery (U2M browser flow AND M2M client-credentials
683+
// — Azure workspaces serve `/oidc/.well-known/...`), but has NO Entra-direct
684+
// browser U2M; Entra-direct SP M2M maps to the kernel's dedicated
685+
// azure-sp-m2m. On a non-Azure host these flags are inert (the in-house flow
686+
// is the only one), matching Thrift.
687+
const entraDirect = isAzureHost(options.host) && oauth.useDatabricksOAuthInAzure !== true;
688+
if (entraDirect) {
689+
if (oauth.oauthClientSecret === undefined) {
690+
// Entra-direct browser U2M — the kernel has no direct-Entra U2M flow.
691+
throw new HiveDriverError(
692+
'kernel backend: Azure AD (Entra-direct) OAuth U2M is not supported. Set ' +
693+
'`useDatabricksOAuthInAzure: true` to use the in-house workspace-federated browser ' +
694+
'flow (which the kernel runs against Azure Databricks workspaces), or use the Thrift ' +
695+
'backend (default) for the Entra-direct flow.',
696+
);
697+
}
698+
// Entra-direct service-principal M2M → the kernel's azure-sp-m2m. The Entra
699+
// SP credentials ride the generic `oauthClientId` / `oauthClientSecret`
700+
// (Thrift convention); forward them as `azureClientId` / `azureClientSecret`.
701+
// `azureTenantId` is optional — the kernel auto-discovers it from the
702+
// workspace `/aad/auth` redirect when omitted.
703+
const azureClientId = oauth.oauthClientId;
704+
if (azureClientId === undefined) {
705+
throw new HiveDriverError(
706+
'kernel backend: Azure service-principal M2M requires `oauthClientId` (the Entra ' +
707+
'app-registration client id) alongside `oauthClientSecret`.',
708+
);
709+
}
710+
const azure = {
711+
...base,
712+
authMode: 'AzureSpM2m' as const,
713+
azureClientId,
714+
azureClientSecret: oauth.oauthClientSecret,
715+
};
716+
return oauth.azureTenantId !== undefined ? { ...azure, azureTenantId: oauth.azureTenantId } : azure;
641717
}
642718

643719
// Flow selector + client-id resolution mirror the Thrift driver EXACTLY

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

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,23 +127,56 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => {
127127
expect((native as { oauthClientId?: string }).oauthClientId).to.equal('client-uuid');
128128
});
129129

130-
it('rejects azureTenantId with a clear Entra-direct-out-of-scope error', () => {
130+
it('routes Azure host + secret (Entra-direct default) to azure-sp-m2m, forwarding the tenant', () => {
131+
// On an Azure host with no `useDatabricksOAuthInAzure`, the default is the
132+
// Entra-direct flow — for M2M that is the kernel's azure-sp-m2m. The Entra
133+
// SP creds ride oauthClientId/oauthClientSecret; azureTenantId is forwarded
134+
// (optional — the kernel auto-discovers it when omitted).
131135
const opts: ConnectionOptions = {
132136
host: 'adb-12345.0.azuredatabricks.net',
133137
path: '/sql/1.0/warehouses/abc',
134138
authType: 'databricks-oauth',
135-
oauthClientId: 'client-uuid',
136-
oauthClientSecret: 'dose-fake-secret',
139+
oauthClientId: 'entra-app-id',
140+
oauthClientSecret: 'entra-secret',
137141
azureTenantId: 'tenant-uuid',
138142
};
139143

140-
expect(() => buildKernelConnectionOptions(opts)).to.throw(
141-
HiveDriverError,
142-
/Azure-direct OAuth.*is not supported/,
143-
);
144+
const native = buildKernelConnectionOptions(opts);
145+
expectNativeConnectionOptions(native, {
146+
hostName: 'adb-12345.0.azuredatabricks.net',
147+
httpPath: '/sql/1.0/warehouses/abc',
148+
intervalsAsString: true,
149+
authMode: 'AzureSpM2m',
150+
azureClientId: 'entra-app-id',
151+
azureClientSecret: 'entra-secret',
152+
azureTenantId: 'tenant-uuid',
153+
});
154+
});
155+
156+
it('routes Azure host + secret without a tenant to azure-sp-m2m (kernel auto-discovers)', () => {
157+
const opts: ConnectionOptions = {
158+
host: 'adb-12345.0.azuredatabricks.net',
159+
path: '/sql/1.0/warehouses/abc',
160+
authType: 'databricks-oauth',
161+
oauthClientId: 'entra-app-id',
162+
oauthClientSecret: 'entra-secret',
163+
};
164+
165+
const native = buildKernelConnectionOptions(opts);
166+
expectNativeConnectionOptions(native, {
167+
hostName: 'adb-12345.0.azuredatabricks.net',
168+
httpPath: '/sql/1.0/warehouses/abc',
169+
intervalsAsString: true,
170+
authMode: 'AzureSpM2m',
171+
azureClientId: 'entra-app-id',
172+
azureClientSecret: 'entra-secret',
173+
});
144174
});
145175

146-
it('rejects useDatabricksOAuthInAzure with the same Entra-direct error', () => {
176+
it('routes Azure host + useDatabricksOAuthInAzure:true + secret to in-house OAuthM2m', () => {
177+
// `useDatabricksOAuthInAzure: true` opts into the in-house
178+
// (workspace-federated) flow — for M2M that is the kernel's generic
179+
// workspace-OIDC client-credentials (OAuthM2m), which works on Azure hosts.
147180
const opts: ConnectionOptions = {
148181
host: 'adb-12345.0.azuredatabricks.net',
149182
path: '/sql/1.0/warehouses/abc',
@@ -153,10 +186,16 @@ describe('KernelAuth + KernelBackend — OAuth M2M auth flow', () => {
153186
useDatabricksOAuthInAzure: true,
154187
};
155188

156-
expect(() => buildKernelConnectionOptions(opts)).to.throw(
157-
HiveDriverError,
158-
/Azure-direct OAuth.*is not supported/,
159-
);
189+
const native = buildKernelConnectionOptions(opts);
190+
expectNativeConnectionOptions(native, {
191+
hostName: 'adb-12345.0.azuredatabricks.net',
192+
httpPath: '/sql/1.0/warehouses/abc',
193+
intervalsAsString: true,
194+
authMode: 'OAuthM2m',
195+
oauthClientId: 'client-uuid',
196+
oauthClientSecret: 'dose-fake-secret',
197+
oauthScopes: ['all-apis'],
198+
});
160199
});
161200

162201
it('rejects a `persistence` hook on M2M (no cache needed)', () => {

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => {
100100
expect(native.httpPath).to.equal('/sql/1.0/warehouses/abc');
101101
});
102102

103-
it('rejects azureTenantId on the U2M path with the Entra-direct error', () => {
103+
it('rejects Entra-direct U2M (Azure host, no secret, no useDatabricksOAuthInAzure)', () => {
104+
// On an Azure host the default is the Entra-direct flow; the kernel has no
105+
// direct-Entra browser U2M, so this is rejected with a pointer to the
106+
// in-house flag (or Thrift). azureTenantId does not change that.
104107
const opts: ConnectionOptions = {
105108
host: 'adb-12345.0.azuredatabricks.net',
106109
path: '/sql/1.0/warehouses/abc',
@@ -110,22 +113,30 @@ describe('KernelAuth + KernelBackend — OAuth U2M auth flow', () => {
110113

111114
expect(() => buildKernelConnectionOptions(opts)).to.throw(
112115
HiveDriverError,
113-
/Azure-direct OAuth.*is not supported/,
116+
/Entra-direct\) OAuth U2M is not supported/,
114117
);
115118
});
116119

117-
it('rejects useDatabricksOAuthInAzure on the U2M path', () => {
120+
it('routes Azure host + useDatabricksOAuthInAzure:true (no secret) to in-house OAuthU2m', () => {
121+
// `useDatabricksOAuthInAzure: true` opts into the in-house
122+
// (workspace-federated) browser flow, which the kernel runs against Azure
123+
// Databricks workspaces — so this is the U2M happy path, not a rejection.
118124
const opts: ConnectionOptions = {
119125
host: 'adb-12345.0.azuredatabricks.net',
120126
path: '/sql/1.0/warehouses/abc',
121127
authType: 'databricks-oauth',
122128
useDatabricksOAuthInAzure: true,
123129
};
124130

125-
expect(() => buildKernelConnectionOptions(opts)).to.throw(
126-
HiveDriverError,
127-
/Azure-direct OAuth.*is not supported/,
128-
);
131+
const native = buildKernelConnectionOptions(opts);
132+
expectNativeConnectionOptions(native, {
133+
hostName: 'adb-12345.0.azuredatabricks.net',
134+
httpPath: '/sql/1.0/warehouses/abc',
135+
intervalsAsString: true,
136+
authMode: 'OAuthU2m',
137+
oauthRedirectPort: 8030,
138+
oauthScopes: ['sql', 'offline_access'],
139+
});
129140
});
130141

131142
it('rejects a `persistence` hook on U2M citing the AuthConfig::External kernel-plumbing gap', () => {

0 commit comments

Comments
 (0)