@dbx-tools/appkit 0.6.48 → 0.6.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/index.ts +3 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -1
- package/lib/src/identity.d.ts +119 -0
- package/lib/src/identity.js +137 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/identity.ts +159 -0
package/README.md
CHANGED
|
@@ -38,6 +38,9 @@ Use this package when the friction is around bootstrapping and reuse:
|
|
|
38
38
|
- AppKit does not own your local CLI flags, bundle validation output, or
|
|
39
39
|
`app.yaml`; `config.resolveConfigValue()` gives setup scripts one resolution
|
|
40
40
|
path across those sources.
|
|
41
|
+
- AppKit's `asUser(req)` throws outside `NODE_ENV=development` when a request
|
|
42
|
+
carries no OBO token; `identity` makes falling back to the service principal a
|
|
43
|
+
configured, per-request decision instead of a `NODE_ENV` side effect.
|
|
41
44
|
|
|
42
45
|
## Create An Auto-Configured App
|
|
43
46
|
|
|
@@ -231,6 +234,58 @@ Pass a second argument to report progress on your own logger. The grants are
|
|
|
231
234
|
skipped inside a Databricks App, and any failure is logged rather than thrown so
|
|
232
235
|
a degraded cache never blocks startup.
|
|
233
236
|
|
|
237
|
+
## Choose The Request Identity
|
|
238
|
+
|
|
239
|
+
AppKit gives a plugin two identities: the ambient service context (the app's own
|
|
240
|
+
service principal) and a per-request user context entered with `asUser(req)`,
|
|
241
|
+
which authenticates on-behalf-of (OBO) using the token the Databricks front door
|
|
242
|
+
forwards on `x-forwarded-access-token`. When that header is absent, AppKit's
|
|
243
|
+
behaviour depends entirely on `NODE_ENV`:
|
|
244
|
+
|
|
245
|
+
| `NODE_ENV` | `asUser(req)` with no `x-forwarded-access-token` |
|
|
246
|
+
| ------------- | ------------------------------------------------------ |
|
|
247
|
+
| `development` | logs a warning, silently runs as the service principal |
|
|
248
|
+
| anything else | throws `AuthenticationError: Missing user token` |
|
|
249
|
+
|
|
250
|
+
That throw is right for an app behind the front door, where a missing token means
|
|
251
|
+
something is broken. It is fatal for an app whose traffic legitimately arrives
|
|
252
|
+
without one - a public tunnel where callers authenticate by email code
|
|
253
|
+
([`@dbx-tools/cli-tunnel`](../../cli/tunnel)), or a bot channel that validates its
|
|
254
|
+
own inbound JWT (`POST /api/teams/messages`). Those apps must not run with
|
|
255
|
+
`NODE_ENV=development` just to get the fallback, since that flag also relaxes
|
|
256
|
+
secure cookies and unlocks other dev-only escape hatches.
|
|
257
|
+
|
|
258
|
+
`identity` is that decision, made explicit:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { identity } from "@dbx-tools/appkit";
|
|
262
|
+
|
|
263
|
+
const mode = identity.resolveIdentityMode(config.identity, "MY_APP_IDENTITY");
|
|
264
|
+
|
|
265
|
+
// One call decides whether this request enters `asUser`.
|
|
266
|
+
const scoped = identity.useServicePrincipal(mode, req) ? this : this.asUser(req);
|
|
267
|
+
const rows = await scoped.executeQuery(sql);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
| Mode | Behaviour |
|
|
271
|
+
| ------------------- | ---------------------------------------------------------------------------- |
|
|
272
|
+
| `user` (default) | Always OBO. Per-user attribution and per-user Genie / Unity Catalog filters. |
|
|
273
|
+
| `service-principal` | Always the app's own identity. Needs no OBO scopes and serves any caller. |
|
|
274
|
+
| `auto` | OBO when the request carries a usable token, service principal otherwise. |
|
|
275
|
+
|
|
276
|
+
`auto` decides per REQUEST, not per boot, because one container can serve both
|
|
277
|
+
doors at once - the tunnel gate and the platform front door share a port. A
|
|
278
|
+
boot-time flag would have to be wrong for one of them. An unrecognized configured
|
|
279
|
+
value throws a `ConfigurationError` rather than falling back, because a typo
|
|
280
|
+
(`"obo"`, `"sp"`) would otherwise keep serving the very error the option was set
|
|
281
|
+
to avoid.
|
|
282
|
+
|
|
283
|
+
Running as the service principal does not change WHO the request belongs to. The
|
|
284
|
+
caller still arrives on `x-forwarded-user` / `x-forwarded-email` (read them with
|
|
285
|
+
`identity.requestUserId()` / `requestUserEmail()`), so memory threads, cache
|
|
286
|
+
namespaces, and trace attribution stay per-user - only the Databricks credential
|
|
287
|
+
is shared. `@dbx-tools/appkit-mastra` exposes this as its `genieIdentity` option.
|
|
288
|
+
|
|
234
289
|
## Modules
|
|
235
290
|
|
|
236
291
|
| Module | Responsibility |
|
|
@@ -243,6 +298,7 @@ a degraded cache never blocks startup.
|
|
|
243
298
|
| `databricks` | App env detection and SDK context cancellation adapters. |
|
|
244
299
|
| `plugin` | Typed AppKit plugin data, instance, and required-instance lookup. |
|
|
245
300
|
| `provision` | Cache schema provisioning helpers. |
|
|
301
|
+
| `identity` | OBO-vs-service-principal request identity: modes, resolution, and the forwarded headers. |
|
|
246
302
|
|
|
247
303
|
The shell-facing wrapper for auto-config is
|
|
248
304
|
[`@dbx-tools/cli-appkit-env`](../../cli/appkit-env). Higher-level agent composition
|
package/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * as appkit from "./src/appkit.ts";
|
|
|
6
6
|
export * as config from "./src/config.ts";
|
|
7
7
|
export * as createApp from "./src/create-app.ts";
|
|
8
8
|
export * as databricks from "./src/databricks.ts";
|
|
9
|
+
export * as identity from "./src/identity.ts";
|
|
9
10
|
export * as lakebaseResolver from "./src/lakebase-resolver.ts";
|
|
10
11
|
export * as pgaddress from "./src/pgaddress.ts";
|
|
11
12
|
export * as plugin from "./src/plugin.ts";
|
|
@@ -15,6 +16,8 @@ export type { BundleValidateJson, ConfigFile, ConfigSource, ConfigMapValue, Reso
|
|
|
15
16
|
export type { AutoConfigureMode, CreateAppConfig } from "./src/create-app.ts";
|
|
16
17
|
export { MAX_TCP_PORT } from "./src/databricks.ts";
|
|
17
18
|
export type { ContextLike } from "./src/databricks.ts";
|
|
19
|
+
export { ACCESS_TOKEN_HEADER, USER_ID_HEADER, USER_EMAIL_HEADER, IDENTITY_MODES, DEFAULT_IDENTITY_MODE } from "./src/identity.ts";
|
|
20
|
+
export type { IdentityMode, HeaderBearing } from "./src/identity.ts";
|
|
18
21
|
export type { LakebaseResolverInputs, LakebaseConnection } from "./src/lakebase-resolver.ts";
|
|
19
22
|
export { SSL_MODES } from "./src/pgaddress.ts";
|
|
20
23
|
export type { SslMode, LakebaseConnectionInputs, ParsedAddress } from "./src/pgaddress.ts";
|
package/lib/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * as appkit from "./src/appkit.ts";
|
|
|
2
2
|
export * as config from "./src/config.ts";
|
|
3
3
|
export * as createApp from "./src/create-app.ts";
|
|
4
4
|
export * as databricks from "./src/databricks.ts";
|
|
5
|
+
export * as identity from "./src/identity.ts";
|
|
5
6
|
export * as lakebaseResolver from "./src/lakebase-resolver.ts";
|
|
6
7
|
export * as pgaddress from "./src/pgaddress.ts";
|
|
7
8
|
export * as plugin from "./src/plugin.ts";
|
|
@@ -11,6 +12,8 @@ export type { BundleValidateJson, ConfigFile, ConfigSource, ConfigMapValue, Reso
|
|
|
11
12
|
export type { AutoConfigureMode, CreateAppConfig } from "./src/create-app.ts";
|
|
12
13
|
export { MAX_TCP_PORT } from "./src/databricks.ts";
|
|
13
14
|
export type { ContextLike } from "./src/databricks.ts";
|
|
15
|
+
export { ACCESS_TOKEN_HEADER, USER_ID_HEADER, USER_EMAIL_HEADER, IDENTITY_MODES, DEFAULT_IDENTITY_MODE } from "./src/identity.ts";
|
|
16
|
+
export type { IdentityMode, HeaderBearing } from "./src/identity.ts";
|
|
14
17
|
export type { LakebaseResolverInputs, LakebaseConnection } from "./src/lakebase-resolver.ts";
|
|
15
18
|
export { SSL_MODES } from "./src/pgaddress.ts";
|
|
16
19
|
export type { SslMode, LakebaseConnectionInputs, ParsedAddress } from "./src/pgaddress.ts";
|
package/lib/index.js
CHANGED
|
@@ -5,10 +5,12 @@ export * as appkit from "./src/appkit.js";
|
|
|
5
5
|
export * as config from "./src/config.js";
|
|
6
6
|
export * as createApp from "./src/create-app.js";
|
|
7
7
|
export * as databricks from "./src/databricks.js";
|
|
8
|
+
export * as identity from "./src/identity.js";
|
|
8
9
|
export * as lakebaseResolver from "./src/lakebase-resolver.js";
|
|
9
10
|
export * as pgaddress from "./src/pgaddress.js";
|
|
10
11
|
export * as plugin from "./src/plugin.js";
|
|
11
12
|
export * as provision from "./src/provision.js";
|
|
12
13
|
export { MAX_TCP_PORT } from "./src/databricks.js";
|
|
14
|
+
export { ACCESS_TOKEN_HEADER, USER_ID_HEADER, USER_EMAIL_HEADER, IDENTITY_MODES, DEFAULT_IDENTITY_MODE } from "./src/identity.js";
|
|
13
15
|
export { SSL_MODES } from "./src/pgaddress.js";
|
|
14
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
16
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssTUFBTSxNQUFNLGlCQUFpQixDQUFDO0FBQzFDLE9BQU8sS0FBSyxNQUFNLE1BQU0saUJBQWlCLENBQUM7QUFDMUMsT0FBTyxLQUFLLFNBQVMsTUFBTSxxQkFBcUIsQ0FBQztBQUNqRCxPQUFPLEtBQUssVUFBVSxNQUFNLHFCQUFxQixDQUFDO0FBQ2xELE9BQU8sS0FBSyxRQUFRLE1BQU0sbUJBQW1CLENBQUM7QUFDOUMsT0FBTyxLQUFLLGdCQUFnQixNQUFNLDRCQUE0QixDQUFDO0FBQy9ELE9BQU8sS0FBSyxTQUFTLE1BQU0sb0JBQW9CLENBQUM7QUFDaEQsT0FBTyxLQUFLLE1BQU0sTUFBTSxpQkFBaUIsQ0FBQztBQUMxQyxPQUFPLEtBQUssU0FBUyxNQUFNLG9CQUFvQixDQUFDO0FBSWhELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUVuRCxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsY0FBYyxFQUFFLGlCQUFpQixFQUFFLGNBQWMsRUFBRSxxQkFBcUIsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBR2xJLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIEdFTkVSQVRFRCBieSBwcm9qZW4gd2F0Y2ggLSBETyBOT1QgRURJVC5cbi8vIFJlZ2VuZXJhdGVkIGZyb20gdGhlIGV4cG9ydGluZyBtb2R1bGVzIGluIC4vc3JjLlxuLy8gSGFuZCBlZGl0cyBhcmUgb3ZlcndyaXR0ZW4gb24gdGhlIG5leHQgd2F0Y2g7IHRoaXMgZmlsZSBpcyByZWFkLW9ubHkuXG5cbmV4cG9ydCAqIGFzIGFwcGtpdCBmcm9tIFwiLi9zcmMvYXBwa2l0LnRzXCI7XG5leHBvcnQgKiBhcyBjb25maWcgZnJvbSBcIi4vc3JjL2NvbmZpZy50c1wiO1xuZXhwb3J0ICogYXMgY3JlYXRlQXBwIGZyb20gXCIuL3NyYy9jcmVhdGUtYXBwLnRzXCI7XG5leHBvcnQgKiBhcyBkYXRhYnJpY2tzIGZyb20gXCIuL3NyYy9kYXRhYnJpY2tzLnRzXCI7XG5leHBvcnQgKiBhcyBpZGVudGl0eSBmcm9tIFwiLi9zcmMvaWRlbnRpdHkudHNcIjtcbmV4cG9ydCAqIGFzIGxha2ViYXNlUmVzb2x2ZXIgZnJvbSBcIi4vc3JjL2xha2ViYXNlLXJlc29sdmVyLnRzXCI7XG5leHBvcnQgKiBhcyBwZ2FkZHJlc3MgZnJvbSBcIi4vc3JjL3BnYWRkcmVzcy50c1wiO1xuZXhwb3J0ICogYXMgcGx1Z2luIGZyb20gXCIuL3NyYy9wbHVnaW4udHNcIjtcbmV4cG9ydCAqIGFzIHByb3Zpc2lvbiBmcm9tIFwiLi9zcmMvcHJvdmlzaW9uLnRzXCI7XG5leHBvcnQgdHlwZSB7IEV4ZWN1dGlvbkNvbnRleHRMaWtlLCBXb3Jrc3BhY2VDbGllbnRMaWtlIH0gZnJvbSBcIi4vc3JjL2FwcGtpdC50c1wiO1xuZXhwb3J0IHR5cGUgeyBCdW5kbGVWYWxpZGF0ZUpzb24sIENvbmZpZ0ZpbGUsIENvbmZpZ1NvdXJjZSwgQ29uZmlnTWFwVmFsdWUsIFJlc29sdmVDb25maWdWYWx1ZU9wdGlvbnMgfSBmcm9tIFwiLi9zcmMvY29uZmlnLnRzXCI7XG5leHBvcnQgdHlwZSB7IEF1dG9Db25maWd1cmVNb2RlLCBDcmVhdGVBcHBDb25maWcgfSBmcm9tIFwiLi9zcmMvY3JlYXRlLWFwcC50c1wiO1xuZXhwb3J0IHsgTUFYX1RDUF9QT1JUIH0gZnJvbSBcIi4vc3JjL2RhdGFicmlja3MudHNcIjtcbmV4cG9ydCB0eXBlIHsgQ29udGV4dExpa2UgfSBmcm9tIFwiLi9zcmMvZGF0YWJyaWNrcy50c1wiO1xuZXhwb3J0IHsgQUNDRVNTX1RPS0VOX0hFQURFUiwgVVNFUl9JRF9IRUFERVIsIFVTRVJfRU1BSUxfSEVBREVSLCBJREVOVElUWV9NT0RFUywgREVGQVVMVF9JREVOVElUWV9NT0RFIH0gZnJvbSBcIi4vc3JjL2lkZW50aXR5LnRzXCI7XG5leHBvcnQgdHlwZSB7IElkZW50aXR5TW9kZSwgSGVhZGVyQmVhcmluZyB9IGZyb20gXCIuL3NyYy9pZGVudGl0eS50c1wiO1xuZXhwb3J0IHR5cGUgeyBMYWtlYmFzZVJlc29sdmVySW5wdXRzLCBMYWtlYmFzZUNvbm5lY3Rpb24gfSBmcm9tIFwiLi9zcmMvbGFrZWJhc2UtcmVzb2x2ZXIudHNcIjtcbmV4cG9ydCB7IFNTTF9NT0RFUyB9IGZyb20gXCIuL3NyYy9wZ2FkZHJlc3MudHNcIjtcbmV4cG9ydCB0eXBlIHsgU3NsTW9kZSwgTGFrZWJhc2VDb25uZWN0aW9uSW5wdXRzLCBQYXJzZWRBZGRyZXNzIH0gZnJvbSBcIi4vc3JjL3BnYWRkcmVzcy50c1wiO1xuZXhwb3J0IHR5cGUgeyBQbHVnaW5Db250ZXh0TGlrZSB9IGZyb20gXCIuL3NyYy9wbHVnaW4udHNcIjtcbiJdfQ==
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Databricks identity a request's workspace calls run as.
|
|
3
|
+
*
|
|
4
|
+
* AppKit gives a plugin two identities: the ambient SERVICE context (the app's
|
|
5
|
+
* own service principal) and a per-request USER context entered with
|
|
6
|
+
* `asUser(req)`, which authenticates as the caller on-behalf-of (OBO). The
|
|
7
|
+
* choice is one call, so the whole decision is "do we enter `asUser` for this
|
|
8
|
+
* request".
|
|
9
|
+
*
|
|
10
|
+
* `asUser(req)` needs the OBO token the platform front door forwards on
|
|
11
|
+
* {@link ACCESS_TOKEN_HEADER}. Measured against the installed AppKit, its
|
|
12
|
+
* behavior when that header is absent depends ENTIRELY on `NODE_ENV`:
|
|
13
|
+
*
|
|
14
|
+
* | `NODE_ENV` | no `x-forwarded-access-token` |
|
|
15
|
+
* | ------------- | ---------------------------------------------------- |
|
|
16
|
+
* | `development` | logs a warning, silently runs as the service principal |
|
|
17
|
+
* | anything else | throws `AuthenticationError: Missing user token` |
|
|
18
|
+
*
|
|
19
|
+
* That production throw is correct for an app behind the Databricks front door,
|
|
20
|
+
* where a missing token means something is wrong. It is fatal for an app whose
|
|
21
|
+
* traffic legitimately arrives WITHOUT one:
|
|
22
|
+
*
|
|
23
|
+
* - a public tunnel (`@dbx-tools/cli-tunnel`), where callers authenticate by
|
|
24
|
+
* email OTP and no OBO token exists to forward - the gate can prove WHO the
|
|
25
|
+
* caller is, but it cannot mint a Databricks credential for them;
|
|
26
|
+
* - any reverse proxy, webhook, or bot channel (`POST /api/teams/messages`)
|
|
27
|
+
* that authenticates its own way.
|
|
28
|
+
*
|
|
29
|
+
* Such an app must not run with `NODE_ENV=development` just to get the fallback:
|
|
30
|
+
* that flag also relaxes secure cookies, AppKit's own dev affordances, and
|
|
31
|
+
* `allowUnauthenticated` escape hatches. Hence {@link IdentityMode}:
|
|
32
|
+
*
|
|
33
|
+
* - `"user"` - always OBO. Per-user attribution and per-user Genie / Unity
|
|
34
|
+
* Catalog row filters. Correct when every caller is a workspace member.
|
|
35
|
+
* - `"service-principal"` - always the app's own identity. Needs no OBO
|
|
36
|
+
* scopes and works for any caller, at the cost of per-user data scoping.
|
|
37
|
+
* - `"auto"` - OBO when the request actually carries a usable OBO token,
|
|
38
|
+
* the service principal otherwise. One deployment then serves BOTH doors
|
|
39
|
+
* correctly: front-door requests keep full per-user scoping, while tunnel /
|
|
40
|
+
* webhook requests degrade to the service principal instead of 500ing.
|
|
41
|
+
*
|
|
42
|
+
* `"auto"` decides per REQUEST, not per boot, because a single container serves
|
|
43
|
+
* both doors at once - the tunnel gate and the platform front door share a port
|
|
44
|
+
* (see `@dbx-tools/cli-tunnel`). A boot-time flag would have to be wrong for one
|
|
45
|
+
* of them.
|
|
46
|
+
*
|
|
47
|
+
* What the service principal does NOT change is WHO the request belongs to. The
|
|
48
|
+
* caller's identity still arrives on {@link USER_ID_HEADER} /
|
|
49
|
+
* {@link USER_EMAIL_HEADER}, so memory threads, cache namespaces, and trace
|
|
50
|
+
* attribution stay per-user. Only the Databricks credential is shared.
|
|
51
|
+
*
|
|
52
|
+
* @module
|
|
53
|
+
*/
|
|
54
|
+
import { type EnvKey } from "@dbx-tools/shared-core";
|
|
55
|
+
/**
|
|
56
|
+
* Header the Databricks Apps front door forwards the caller's OBO token on.
|
|
57
|
+
* Its presence is what makes `asUser(req)` viable, so it is the signal
|
|
58
|
+
* {@link useServicePrincipal} reads in `"auto"` mode.
|
|
59
|
+
*
|
|
60
|
+
* Re-exported from `@dbx-tools/shared-core`'s `token` module (which reads the
|
|
61
|
+
* same header to decode OAuth scopes) rather than re-spelled, so the name for
|
|
62
|
+
* this wire contract exists once.
|
|
63
|
+
*/
|
|
64
|
+
export declare const ACCESS_TOKEN_HEADER = "x-forwarded-access-token";
|
|
65
|
+
/** Header the Databricks Apps front door forwards the caller's user id on. */
|
|
66
|
+
export declare const USER_ID_HEADER = "x-forwarded-user";
|
|
67
|
+
/** Header the Databricks Apps front door forwards the caller's email on. */
|
|
68
|
+
export declare const USER_EMAIL_HEADER = "x-forwarded-email";
|
|
69
|
+
/** Identity a request's Databricks calls run as. See the module docs. */
|
|
70
|
+
export type IdentityMode = "user" | "service-principal" | "auto";
|
|
71
|
+
/** Every accepted {@link IdentityMode}, in the order docs and schemas list them. */
|
|
72
|
+
export declare const IDENTITY_MODES: readonly IdentityMode[];
|
|
73
|
+
/**
|
|
74
|
+
* Default mode. `"user"` keeps OBO the only identity unless an app opts in, so
|
|
75
|
+
* adopting this option can never silently widen an existing app's data access.
|
|
76
|
+
*/
|
|
77
|
+
export declare const DEFAULT_IDENTITY_MODE: IdentityMode;
|
|
78
|
+
/** The subset of `express.Request` this module reads - one header lookup. */
|
|
79
|
+
export interface HeaderBearing {
|
|
80
|
+
header(name: string): string | undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a configured mode: explicit config value, then the first non-empty
|
|
84
|
+
* variable among `envKeys`, then {@link DEFAULT_IDENTITY_MODE}.
|
|
85
|
+
*
|
|
86
|
+
* An unrecognized value throws rather than falling back. Falling back would
|
|
87
|
+
* silently keep serving OBO - and the production `AuthenticationError` it
|
|
88
|
+
* produces - to exactly the callers the option was set to accommodate, and a
|
|
89
|
+
* typo (`"obo"`, `"sp"`) is the likeliest way to get one.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resolveIdentityMode(configured: string | undefined, envKeys: EnvKey, field?: string): IdentityMode;
|
|
92
|
+
/**
|
|
93
|
+
* The OBO token on `req`, or `undefined`. Read through the same trim as every
|
|
94
|
+
* other header, so a header present-but-blank (which some proxies emit for an
|
|
95
|
+
* unset upstream value) counts as absent rather than as a token that fails at
|
|
96
|
+
* the first Databricks call.
|
|
97
|
+
*/
|
|
98
|
+
export declare function requestAccessToken(req: HeaderBearing | undefined): string | undefined;
|
|
99
|
+
/** The forwarded user id on `req`, or `undefined`. */
|
|
100
|
+
export declare function requestUserId(req: HeaderBearing | undefined): string | undefined;
|
|
101
|
+
/** The forwarded user email on `req`, or `undefined`. */
|
|
102
|
+
export declare function requestUserEmail(req: HeaderBearing | undefined): string | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Whether `req` should run its Databricks calls as the app service principal
|
|
105
|
+
* rather than OBO.
|
|
106
|
+
*
|
|
107
|
+
* - `"service-principal"` -> always `true`.
|
|
108
|
+
* - `"user"` -> always `false`, even with no token. The mode is an explicit
|
|
109
|
+
* assertion that every caller is OBO-capable, so a missing token is a real
|
|
110
|
+
* error and must surface as AppKit's `AuthenticationError` rather than being
|
|
111
|
+
* quietly downgraded to shared data access.
|
|
112
|
+
* - `"auto"` -> `true` only when the request carries no usable OBO token.
|
|
113
|
+
*
|
|
114
|
+
* A request AppKit will not accept for OBO anyway (no token) can never be
|
|
115
|
+
* served by entering `asUser`, so in `"auto"` mode the token check is the whole
|
|
116
|
+
* decision: it is the same condition AppKit itself branches on, just resolved
|
|
117
|
+
* before it can throw.
|
|
118
|
+
*/
|
|
119
|
+
export declare function useServicePrincipal(mode: IdentityMode, req?: HeaderBearing): boolean;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Databricks identity a request's workspace calls run as.
|
|
3
|
+
*
|
|
4
|
+
* AppKit gives a plugin two identities: the ambient SERVICE context (the app's
|
|
5
|
+
* own service principal) and a per-request USER context entered with
|
|
6
|
+
* `asUser(req)`, which authenticates as the caller on-behalf-of (OBO). The
|
|
7
|
+
* choice is one call, so the whole decision is "do we enter `asUser` for this
|
|
8
|
+
* request".
|
|
9
|
+
*
|
|
10
|
+
* `asUser(req)` needs the OBO token the platform front door forwards on
|
|
11
|
+
* {@link ACCESS_TOKEN_HEADER}. Measured against the installed AppKit, its
|
|
12
|
+
* behavior when that header is absent depends ENTIRELY on `NODE_ENV`:
|
|
13
|
+
*
|
|
14
|
+
* | `NODE_ENV` | no `x-forwarded-access-token` |
|
|
15
|
+
* | ------------- | ---------------------------------------------------- |
|
|
16
|
+
* | `development` | logs a warning, silently runs as the service principal |
|
|
17
|
+
* | anything else | throws `AuthenticationError: Missing user token` |
|
|
18
|
+
*
|
|
19
|
+
* That production throw is correct for an app behind the Databricks front door,
|
|
20
|
+
* where a missing token means something is wrong. It is fatal for an app whose
|
|
21
|
+
* traffic legitimately arrives WITHOUT one:
|
|
22
|
+
*
|
|
23
|
+
* - a public tunnel (`@dbx-tools/cli-tunnel`), where callers authenticate by
|
|
24
|
+
* email OTP and no OBO token exists to forward - the gate can prove WHO the
|
|
25
|
+
* caller is, but it cannot mint a Databricks credential for them;
|
|
26
|
+
* - any reverse proxy, webhook, or bot channel (`POST /api/teams/messages`)
|
|
27
|
+
* that authenticates its own way.
|
|
28
|
+
*
|
|
29
|
+
* Such an app must not run with `NODE_ENV=development` just to get the fallback:
|
|
30
|
+
* that flag also relaxes secure cookies, AppKit's own dev affordances, and
|
|
31
|
+
* `allowUnauthenticated` escape hatches. Hence {@link IdentityMode}:
|
|
32
|
+
*
|
|
33
|
+
* - `"user"` - always OBO. Per-user attribution and per-user Genie / Unity
|
|
34
|
+
* Catalog row filters. Correct when every caller is a workspace member.
|
|
35
|
+
* - `"service-principal"` - always the app's own identity. Needs no OBO
|
|
36
|
+
* scopes and works for any caller, at the cost of per-user data scoping.
|
|
37
|
+
* - `"auto"` - OBO when the request actually carries a usable OBO token,
|
|
38
|
+
* the service principal otherwise. One deployment then serves BOTH doors
|
|
39
|
+
* correctly: front-door requests keep full per-user scoping, while tunnel /
|
|
40
|
+
* webhook requests degrade to the service principal instead of 500ing.
|
|
41
|
+
*
|
|
42
|
+
* `"auto"` decides per REQUEST, not per boot, because a single container serves
|
|
43
|
+
* both doors at once - the tunnel gate and the platform front door share a port
|
|
44
|
+
* (see `@dbx-tools/cli-tunnel`). A boot-time flag would have to be wrong for one
|
|
45
|
+
* of them.
|
|
46
|
+
*
|
|
47
|
+
* What the service principal does NOT change is WHO the request belongs to. The
|
|
48
|
+
* caller's identity still arrives on {@link USER_ID_HEADER} /
|
|
49
|
+
* {@link USER_EMAIL_HEADER}, so memory threads, cache namespaces, and trace
|
|
50
|
+
* attribution stay per-user. Only the Databricks credential is shared.
|
|
51
|
+
*
|
|
52
|
+
* @module
|
|
53
|
+
*/
|
|
54
|
+
import { ConfigurationError } from "@databricks/appkit";
|
|
55
|
+
import { env, string, token } from "@dbx-tools/shared-core";
|
|
56
|
+
/**
|
|
57
|
+
* Header the Databricks Apps front door forwards the caller's OBO token on.
|
|
58
|
+
* Its presence is what makes `asUser(req)` viable, so it is the signal
|
|
59
|
+
* {@link useServicePrincipal} reads in `"auto"` mode.
|
|
60
|
+
*
|
|
61
|
+
* Re-exported from `@dbx-tools/shared-core`'s `token` module (which reads the
|
|
62
|
+
* same header to decode OAuth scopes) rather than re-spelled, so the name for
|
|
63
|
+
* this wire contract exists once.
|
|
64
|
+
*/
|
|
65
|
+
export const ACCESS_TOKEN_HEADER = token.ACCESS_TOKEN_HEADER;
|
|
66
|
+
/** Header the Databricks Apps front door forwards the caller's user id on. */
|
|
67
|
+
export const USER_ID_HEADER = token.USER_ID_HEADER;
|
|
68
|
+
/** Header the Databricks Apps front door forwards the caller's email on. */
|
|
69
|
+
export const USER_EMAIL_HEADER = token.USER_EMAIL_HEADER;
|
|
70
|
+
/** Every accepted {@link IdentityMode}, in the order docs and schemas list them. */
|
|
71
|
+
export const IDENTITY_MODES = ["user", "service-principal", "auto"];
|
|
72
|
+
/**
|
|
73
|
+
* Default mode. `"user"` keeps OBO the only identity unless an app opts in, so
|
|
74
|
+
* adopting this option can never silently widen an existing app's data access.
|
|
75
|
+
*/
|
|
76
|
+
export const DEFAULT_IDENTITY_MODE = "user";
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a configured mode: explicit config value, then the first non-empty
|
|
79
|
+
* variable among `envKeys`, then {@link DEFAULT_IDENTITY_MODE}.
|
|
80
|
+
*
|
|
81
|
+
* An unrecognized value throws rather than falling back. Falling back would
|
|
82
|
+
* silently keep serving OBO - and the production `AuthenticationError` it
|
|
83
|
+
* produces - to exactly the callers the option was set to accommodate, and a
|
|
84
|
+
* typo (`"obo"`, `"sp"`) is the likeliest way to get one.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveIdentityMode(configured, envKeys, field = "identity") {
|
|
87
|
+
const raw = env.string(configured, envKeys);
|
|
88
|
+
if (raw === null)
|
|
89
|
+
return DEFAULT_IDENTITY_MODE;
|
|
90
|
+
const mode = raw.toLowerCase();
|
|
91
|
+
if (!IDENTITY_MODES.includes(mode)) {
|
|
92
|
+
const names = typeof envKeys === "string" ? envKeys : envKeys.join(" / ");
|
|
93
|
+
throw new ConfigurationError(`${field} must be one of ${IDENTITY_MODES.join(" | ")} (env: ${names})`, { context: { field, envVar: names, received: raw } });
|
|
94
|
+
}
|
|
95
|
+
return mode;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The OBO token on `req`, or `undefined`. Read through the same trim as every
|
|
99
|
+
* other header, so a header present-but-blank (which some proxies emit for an
|
|
100
|
+
* unset upstream value) counts as absent rather than as a token that fails at
|
|
101
|
+
* the first Databricks call.
|
|
102
|
+
*/
|
|
103
|
+
export function requestAccessToken(req) {
|
|
104
|
+
return string.trimToNull(req?.header(ACCESS_TOKEN_HEADER)) ?? undefined;
|
|
105
|
+
}
|
|
106
|
+
/** The forwarded user id on `req`, or `undefined`. */
|
|
107
|
+
export function requestUserId(req) {
|
|
108
|
+
return string.trimToNull(req?.header(USER_ID_HEADER)) ?? undefined;
|
|
109
|
+
}
|
|
110
|
+
/** The forwarded user email on `req`, or `undefined`. */
|
|
111
|
+
export function requestUserEmail(req) {
|
|
112
|
+
return string.trimToNull(req?.header(USER_EMAIL_HEADER)) ?? undefined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether `req` should run its Databricks calls as the app service principal
|
|
116
|
+
* rather than OBO.
|
|
117
|
+
*
|
|
118
|
+
* - `"service-principal"` -> always `true`.
|
|
119
|
+
* - `"user"` -> always `false`, even with no token. The mode is an explicit
|
|
120
|
+
* assertion that every caller is OBO-capable, so a missing token is a real
|
|
121
|
+
* error and must surface as AppKit's `AuthenticationError` rather than being
|
|
122
|
+
* quietly downgraded to shared data access.
|
|
123
|
+
* - `"auto"` -> `true` only when the request carries no usable OBO token.
|
|
124
|
+
*
|
|
125
|
+
* A request AppKit will not accept for OBO anyway (no token) can never be
|
|
126
|
+
* served by entering `asUser`, so in `"auto"` mode the token check is the whole
|
|
127
|
+
* decision: it is the same condition AppKit itself branches on, just resolved
|
|
128
|
+
* before it can throw.
|
|
129
|
+
*/
|
|
130
|
+
export function useServicePrincipal(mode, req) {
|
|
131
|
+
if (mode === "service-principal")
|
|
132
|
+
return true;
|
|
133
|
+
if (mode === "user")
|
|
134
|
+
return false;
|
|
135
|
+
return requestAccessToken(req) === undefined;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaWRlbnRpdHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaWRlbnRpdHkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FvREc7QUFFSCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUN4RCxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQWUsTUFBTSx3QkFBd0IsQ0FBQztBQUV6RTs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHLEtBQUssQ0FBQyxtQkFBbUIsQ0FBQztBQUU3RCw4RUFBOEU7QUFDOUUsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLEtBQUssQ0FBQyxjQUFjLENBQUM7QUFFbkQsNEVBQTRFO0FBQzVFLE1BQU0sQ0FBQyxNQUFNLGlCQUFpQixHQUFHLEtBQUssQ0FBQyxpQkFBaUIsQ0FBQztBQUt6RCxvRkFBb0Y7QUFDcEYsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUE0QixDQUFDLE1BQU0sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLENBQUMsQ0FBQztBQUU3Rjs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBaUIsTUFBTSxDQUFDO0FBTzFEOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxVQUFVLG1CQUFtQixDQUNqQyxVQUE4QixFQUM5QixPQUFlLEVBQ2YsS0FBSyxHQUFHLFVBQVU7SUFFbEIsTUFBTSxHQUFHLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxVQUFVLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDNUMsSUFBSSxHQUFHLEtBQUssSUFBSTtRQUFFLE9BQU8scUJBQXFCLENBQUM7SUFDL0MsTUFBTSxJQUFJLEdBQUcsR0FBRyxDQUFDLFdBQVcsRUFBa0IsQ0FBQztJQUMvQyxJQUFJLENBQUMsY0FBYyxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQ25DLE1BQU0sS0FBSyxHQUFHLE9BQU8sT0FBTyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzFFLE1BQU0sSUFBSSxrQkFBa0IsQ0FDMUIsR0FBRyxLQUFLLG1CQUFtQixjQUFjLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxVQUFVLEtBQUssR0FBRyxFQUN2RSxFQUFFLE9BQU8sRUFBRSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxHQUFHLEVBQUUsRUFBRSxDQUNyRCxDQUFDO0lBQ0osQ0FBQztJQUNELE9BQU8sSUFBSSxDQUFDO0FBQ2QsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGtCQUFrQixDQUFDLEdBQThCO0lBQy9ELE9BQU8sTUFBTSxDQUFDLFVBQVUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxDQUFDLG1CQUFtQixDQUFDLENBQUMsSUFBSSxTQUFTLENBQUM7QUFDMUUsQ0FBQztBQUVELHNEQUFzRDtBQUN0RCxNQUFNLFVBQVUsYUFBYSxDQUFDLEdBQThCO0lBQzFELE9BQU8sTUFBTSxDQUFDLFVBQVUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxDQUFDLGNBQWMsQ0FBQyxDQUFDLElBQUksU0FBUyxDQUFDO0FBQ3JFLENBQUM7QUFFRCx5REFBeUQ7QUFDekQsTUFBTSxVQUFVLGdCQUFnQixDQUFDLEdBQThCO0lBQzdELE9BQU8sTUFBTSxDQUFDLFVBQVUsQ0FBQyxHQUFHLEVBQUUsTUFBTSxDQUFDLGlCQUFpQixDQUFDLENBQUMsSUFBSSxTQUFTLENBQUM7QUFDeEUsQ0FBQztBQUVEOzs7Ozs7Ozs7Ozs7Ozs7R0FlRztBQUNILE1BQU0sVUFBVSxtQkFBbUIsQ0FBQyxJQUFrQixFQUFFLEdBQW1CO0lBQ3pFLElBQUksSUFBSSxLQUFLLG1CQUFtQjtRQUFFLE9BQU8sSUFBSSxDQUFDO0lBQzlDLElBQUksSUFBSSxLQUFLLE1BQU07UUFBRSxPQUFPLEtBQUssQ0FBQztJQUNsQyxPQUFPLGtCQUFrQixDQUFDLEdBQUcsQ0FBQyxLQUFLLFNBQVMsQ0FBQztBQUMvQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBXaGljaCBEYXRhYnJpY2tzIGlkZW50aXR5IGEgcmVxdWVzdCdzIHdvcmtzcGFjZSBjYWxscyBydW4gYXMuXG4gKlxuICogQXBwS2l0IGdpdmVzIGEgcGx1Z2luIHR3byBpZGVudGl0aWVzOiB0aGUgYW1iaWVudCBTRVJWSUNFIGNvbnRleHQgKHRoZSBhcHAnc1xuICogb3duIHNlcnZpY2UgcHJpbmNpcGFsKSBhbmQgYSBwZXItcmVxdWVzdCBVU0VSIGNvbnRleHQgZW50ZXJlZCB3aXRoXG4gKiBgYXNVc2VyKHJlcSlgLCB3aGljaCBhdXRoZW50aWNhdGVzIGFzIHRoZSBjYWxsZXIgb24tYmVoYWxmLW9mIChPQk8pLiBUaGVcbiAqIGNob2ljZSBpcyBvbmUgY2FsbCwgc28gdGhlIHdob2xlIGRlY2lzaW9uIGlzIFwiZG8gd2UgZW50ZXIgYGFzVXNlcmAgZm9yIHRoaXNcbiAqIHJlcXVlc3RcIi5cbiAqXG4gKiBgYXNVc2VyKHJlcSlgIG5lZWRzIHRoZSBPQk8gdG9rZW4gdGhlIHBsYXRmb3JtIGZyb250IGRvb3IgZm9yd2FyZHMgb25cbiAqIHtAbGluayBBQ0NFU1NfVE9LRU5fSEVBREVSfS4gTWVhc3VyZWQgYWdhaW5zdCB0aGUgaW5zdGFsbGVkIEFwcEtpdCwgaXRzXG4gKiBiZWhhdmlvciB3aGVuIHRoYXQgaGVhZGVyIGlzIGFic2VudCBkZXBlbmRzIEVOVElSRUxZIG9uIGBOT0RFX0VOVmA6XG4gKlxuICogfCBgTk9ERV9FTlZgICAgIHwgbm8gYHgtZm9yd2FyZGVkLWFjY2Vzcy10b2tlbmAgICAgICAgICAgICAgICAgICAgICAgICB8XG4gKiB8IC0tLS0tLS0tLS0tLS0gfCAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIHxcbiAqIHwgYGRldmVsb3BtZW50YCB8IGxvZ3MgYSB3YXJuaW5nLCBzaWxlbnRseSBydW5zIGFzIHRoZSBzZXJ2aWNlIHByaW5jaXBhbCB8XG4gKiB8IGFueXRoaW5nIGVsc2UgfCB0aHJvd3MgYEF1dGhlbnRpY2F0aW9uRXJyb3I6IE1pc3NpbmcgdXNlciB0b2tlbmAgICAgICB8XG4gKlxuICogVGhhdCBwcm9kdWN0aW9uIHRocm93IGlzIGNvcnJlY3QgZm9yIGFuIGFwcCBiZWhpbmQgdGhlIERhdGFicmlja3MgZnJvbnQgZG9vcixcbiAqIHdoZXJlIGEgbWlzc2luZyB0b2tlbiBtZWFucyBzb21ldGhpbmcgaXMgd3JvbmcuIEl0IGlzIGZhdGFsIGZvciBhbiBhcHAgd2hvc2VcbiAqIHRyYWZmaWMgbGVnaXRpbWF0ZWx5IGFycml2ZXMgV0lUSE9VVCBvbmU6XG4gKlxuICogICAtIGEgcHVibGljIHR1bm5lbCAoYEBkYngtdG9vbHMvY2xpLXR1bm5lbGApLCB3aGVyZSBjYWxsZXJzIGF1dGhlbnRpY2F0ZSBieVxuICogICAgIGVtYWlsIE9UUCBhbmQgbm8gT0JPIHRva2VuIGV4aXN0cyB0byBmb3J3YXJkIC0gdGhlIGdhdGUgY2FuIHByb3ZlIFdITyB0aGVcbiAqICAgICBjYWxsZXIgaXMsIGJ1dCBpdCBjYW5ub3QgbWludCBhIERhdGFicmlja3MgY3JlZGVudGlhbCBmb3IgdGhlbTtcbiAqICAgLSBhbnkgcmV2ZXJzZSBwcm94eSwgd2ViaG9vaywgb3IgYm90IGNoYW5uZWwgKGBQT1NUIC9hcGkvdGVhbXMvbWVzc2FnZXNgKVxuICogICAgIHRoYXQgYXV0aGVudGljYXRlcyBpdHMgb3duIHdheS5cbiAqXG4gKiBTdWNoIGFuIGFwcCBtdXN0IG5vdCBydW4gd2l0aCBgTk9ERV9FTlY9ZGV2ZWxvcG1lbnRgIGp1c3QgdG8gZ2V0IHRoZSBmYWxsYmFjazpcbiAqIHRoYXQgZmxhZyBhbHNvIHJlbGF4ZXMgc2VjdXJlIGNvb2tpZXMsIEFwcEtpdCdzIG93biBkZXYgYWZmb3JkYW5jZXMsIGFuZFxuICogYGFsbG93VW5hdXRoZW50aWNhdGVkYCBlc2NhcGUgaGF0Y2hlcy4gSGVuY2Uge0BsaW5rIElkZW50aXR5TW9kZX06XG4gKlxuICogICAtIGBcInVzZXJcImAgLSBhbHdheXMgT0JPLiBQZXItdXNlciBhdHRyaWJ1dGlvbiBhbmQgcGVyLXVzZXIgR2VuaWUgLyBVbml0eVxuICogICAgIENhdGFsb2cgcm93IGZpbHRlcnMuIENvcnJlY3Qgd2hlbiBldmVyeSBjYWxsZXIgaXMgYSB3b3Jrc3BhY2UgbWVtYmVyLlxuICogICAtIGBcInNlcnZpY2UtcHJpbmNpcGFsXCJgIC0gYWx3YXlzIHRoZSBhcHAncyBvd24gaWRlbnRpdHkuIE5lZWRzIG5vIE9CT1xuICogICAgIHNjb3BlcyBhbmQgd29ya3MgZm9yIGFueSBjYWxsZXIsIGF0IHRoZSBjb3N0IG9mIHBlci11c2VyIGRhdGEgc2NvcGluZy5cbiAqICAgLSBgXCJhdXRvXCJgIC0gT0JPIHdoZW4gdGhlIHJlcXVlc3QgYWN0dWFsbHkgY2FycmllcyBhIHVzYWJsZSBPQk8gdG9rZW4sXG4gKiAgICAgdGhlIHNlcnZpY2UgcHJpbmNpcGFsIG90aGVyd2lzZS4gT25lIGRlcGxveW1lbnQgdGhlbiBzZXJ2ZXMgQk9USCBkb29yc1xuICogICAgIGNvcnJlY3RseTogZnJvbnQtZG9vciByZXF1ZXN0cyBrZWVwIGZ1bGwgcGVyLXVzZXIgc2NvcGluZywgd2hpbGUgdHVubmVsIC9cbiAqICAgICB3ZWJob29rIHJlcXVlc3RzIGRlZ3JhZGUgdG8gdGhlIHNlcnZpY2UgcHJpbmNpcGFsIGluc3RlYWQgb2YgNTAwaW5nLlxuICpcbiAqIGBcImF1dG9cImAgZGVjaWRlcyBwZXIgUkVRVUVTVCwgbm90IHBlciBib290LCBiZWNhdXNlIGEgc2luZ2xlIGNvbnRhaW5lciBzZXJ2ZXNcbiAqIGJvdGggZG9vcnMgYXQgb25jZSAtIHRoZSB0dW5uZWwgZ2F0ZSBhbmQgdGhlIHBsYXRmb3JtIGZyb250IGRvb3Igc2hhcmUgYSBwb3J0XG4gKiAoc2VlIGBAZGJ4LXRvb2xzL2NsaS10dW5uZWxgKS4gQSBib290LXRpbWUgZmxhZyB3b3VsZCBoYXZlIHRvIGJlIHdyb25nIGZvciBvbmVcbiAqIG9mIHRoZW0uXG4gKlxuICogV2hhdCB0aGUgc2VydmljZSBwcmluY2lwYWwgZG9lcyBOT1QgY2hhbmdlIGlzIFdITyB0aGUgcmVxdWVzdCBiZWxvbmdzIHRvLiBUaGVcbiAqIGNhbGxlcidzIGlkZW50aXR5IHN0aWxsIGFycml2ZXMgb24ge0BsaW5rIFVTRVJfSURfSEVBREVSfSAvXG4gKiB7QGxpbmsgVVNFUl9FTUFJTF9IRUFERVJ9LCBzbyBtZW1vcnkgdGhyZWFkcywgY2FjaGUgbmFtZXNwYWNlcywgYW5kIHRyYWNlXG4gKiBhdHRyaWJ1dGlvbiBzdGF5IHBlci11c2VyLiBPbmx5IHRoZSBEYXRhYnJpY2tzIGNyZWRlbnRpYWwgaXMgc2hhcmVkLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBDb25maWd1cmF0aW9uRXJyb3IgfSBmcm9tIFwiQGRhdGFicmlja3MvYXBwa2l0XCI7XG5pbXBvcnQgeyBlbnYsIHN0cmluZywgdG9rZW4sIHR5cGUgRW52S2V5IH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWNvcmVcIjtcblxuLyoqXG4gKiBIZWFkZXIgdGhlIERhdGFicmlja3MgQXBwcyBmcm9udCBkb29yIGZvcndhcmRzIHRoZSBjYWxsZXIncyBPQk8gdG9rZW4gb24uXG4gKiBJdHMgcHJlc2VuY2UgaXMgd2hhdCBtYWtlcyBgYXNVc2VyKHJlcSlgIHZpYWJsZSwgc28gaXQgaXMgdGhlIHNpZ25hbFxuICoge0BsaW5rIHVzZVNlcnZpY2VQcmluY2lwYWx9IHJlYWRzIGluIGBcImF1dG9cImAgbW9kZS5cbiAqXG4gKiBSZS1leHBvcnRlZCBmcm9tIGBAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlYCdzIGB0b2tlbmAgbW9kdWxlICh3aGljaCByZWFkcyB0aGVcbiAqIHNhbWUgaGVhZGVyIHRvIGRlY29kZSBPQXV0aCBzY29wZXMpIHJhdGhlciB0aGFuIHJlLXNwZWxsZWQsIHNvIHRoZSBuYW1lIGZvclxuICogdGhpcyB3aXJlIGNvbnRyYWN0IGV4aXN0cyBvbmNlLlxuICovXG5leHBvcnQgY29uc3QgQUNDRVNTX1RPS0VOX0hFQURFUiA9IHRva2VuLkFDQ0VTU19UT0tFTl9IRUFERVI7XG5cbi8qKiBIZWFkZXIgdGhlIERhdGFicmlja3MgQXBwcyBmcm9udCBkb29yIGZvcndhcmRzIHRoZSBjYWxsZXIncyB1c2VyIGlkIG9uLiAqL1xuZXhwb3J0IGNvbnN0IFVTRVJfSURfSEVBREVSID0gdG9rZW4uVVNFUl9JRF9IRUFERVI7XG5cbi8qKiBIZWFkZXIgdGhlIERhdGFicmlja3MgQXBwcyBmcm9udCBkb29yIGZvcndhcmRzIHRoZSBjYWxsZXIncyBlbWFpbCBvbi4gKi9cbmV4cG9ydCBjb25zdCBVU0VSX0VNQUlMX0hFQURFUiA9IHRva2VuLlVTRVJfRU1BSUxfSEVBREVSO1xuXG4vKiogSWRlbnRpdHkgYSByZXF1ZXN0J3MgRGF0YWJyaWNrcyBjYWxscyBydW4gYXMuIFNlZSB0aGUgbW9kdWxlIGRvY3MuICovXG5leHBvcnQgdHlwZSBJZGVudGl0eU1vZGUgPSBcInVzZXJcIiB8IFwic2VydmljZS1wcmluY2lwYWxcIiB8IFwiYXV0b1wiO1xuXG4vKiogRXZlcnkgYWNjZXB0ZWQge0BsaW5rIElkZW50aXR5TW9kZX0sIGluIHRoZSBvcmRlciBkb2NzIGFuZCBzY2hlbWFzIGxpc3QgdGhlbS4gKi9cbmV4cG9ydCBjb25zdCBJREVOVElUWV9NT0RFUzogcmVhZG9ubHkgSWRlbnRpdHlNb2RlW10gPSBbXCJ1c2VyXCIsIFwic2VydmljZS1wcmluY2lwYWxcIiwgXCJhdXRvXCJdO1xuXG4vKipcbiAqIERlZmF1bHQgbW9kZS4gYFwidXNlclwiYCBrZWVwcyBPQk8gdGhlIG9ubHkgaWRlbnRpdHkgdW5sZXNzIGFuIGFwcCBvcHRzIGluLCBzb1xuICogYWRvcHRpbmcgdGhpcyBvcHRpb24gY2FuIG5ldmVyIHNpbGVudGx5IHdpZGVuIGFuIGV4aXN0aW5nIGFwcCdzIGRhdGEgYWNjZXNzLlxuICovXG5leHBvcnQgY29uc3QgREVGQVVMVF9JREVOVElUWV9NT0RFOiBJZGVudGl0eU1vZGUgPSBcInVzZXJcIjtcblxuLyoqIFRoZSBzdWJzZXQgb2YgYGV4cHJlc3MuUmVxdWVzdGAgdGhpcyBtb2R1bGUgcmVhZHMgLSBvbmUgaGVhZGVyIGxvb2t1cC4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgSGVhZGVyQmVhcmluZyB7XG4gIGhlYWRlcihuYW1lOiBzdHJpbmcpOiBzdHJpbmcgfCB1bmRlZmluZWQ7XG59XG5cbi8qKlxuICogUmVzb2x2ZSBhIGNvbmZpZ3VyZWQgbW9kZTogZXhwbGljaXQgY29uZmlnIHZhbHVlLCB0aGVuIHRoZSBmaXJzdCBub24tZW1wdHlcbiAqIHZhcmlhYmxlIGFtb25nIGBlbnZLZXlzYCwgdGhlbiB7QGxpbmsgREVGQVVMVF9JREVOVElUWV9NT0RFfS5cbiAqXG4gKiBBbiB1bnJlY29nbml6ZWQgdmFsdWUgdGhyb3dzIHJhdGhlciB0aGFuIGZhbGxpbmcgYmFjay4gRmFsbGluZyBiYWNrIHdvdWxkXG4gKiBzaWxlbnRseSBrZWVwIHNlcnZpbmcgT0JPIC0gYW5kIHRoZSBwcm9kdWN0aW9uIGBBdXRoZW50aWNhdGlvbkVycm9yYCBpdFxuICogcHJvZHVjZXMgLSB0byBleGFjdGx5IHRoZSBjYWxsZXJzIHRoZSBvcHRpb24gd2FzIHNldCB0byBhY2NvbW1vZGF0ZSwgYW5kIGFcbiAqIHR5cG8gKGBcIm9ib1wiYCwgYFwic3BcImApIGlzIHRoZSBsaWtlbGllc3Qgd2F5IHRvIGdldCBvbmUuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZXNvbHZlSWRlbnRpdHlNb2RlKFxuICBjb25maWd1cmVkOiBzdHJpbmcgfCB1bmRlZmluZWQsXG4gIGVudktleXM6IEVudktleSxcbiAgZmllbGQgPSBcImlkZW50aXR5XCIsXG4pOiBJZGVudGl0eU1vZGUge1xuICBjb25zdCByYXcgPSBlbnYuc3RyaW5nKGNvbmZpZ3VyZWQsIGVudktleXMpO1xuICBpZiAocmF3ID09PSBudWxsKSByZXR1cm4gREVGQVVMVF9JREVOVElUWV9NT0RFO1xuICBjb25zdCBtb2RlID0gcmF3LnRvTG93ZXJDYXNlKCkgYXMgSWRlbnRpdHlNb2RlO1xuICBpZiAoIUlERU5USVRZX01PREVTLmluY2x1ZGVzKG1vZGUpKSB7XG4gICAgY29uc3QgbmFtZXMgPSB0eXBlb2YgZW52S2V5cyA9PT0gXCJzdHJpbmdcIiA/IGVudktleXMgOiBlbnZLZXlzLmpvaW4oXCIgLyBcIik7XG4gICAgdGhyb3cgbmV3IENvbmZpZ3VyYXRpb25FcnJvcihcbiAgICAgIGAke2ZpZWxkfSBtdXN0IGJlIG9uZSBvZiAke0lERU5USVRZX01PREVTLmpvaW4oXCIgfCBcIil9IChlbnY6ICR7bmFtZXN9KWAsXG4gICAgICB7IGNvbnRleHQ6IHsgZmllbGQsIGVudlZhcjogbmFtZXMsIHJlY2VpdmVkOiByYXcgfSB9LFxuICAgICk7XG4gIH1cbiAgcmV0dXJuIG1vZGU7XG59XG5cbi8qKlxuICogVGhlIE9CTyB0b2tlbiBvbiBgcmVxYCwgb3IgYHVuZGVmaW5lZGAuIFJlYWQgdGhyb3VnaCB0aGUgc2FtZSB0cmltIGFzIGV2ZXJ5XG4gKiBvdGhlciBoZWFkZXIsIHNvIGEgaGVhZGVyIHByZXNlbnQtYnV0LWJsYW5rICh3aGljaCBzb21lIHByb3hpZXMgZW1pdCBmb3IgYW5cbiAqIHVuc2V0IHVwc3RyZWFtIHZhbHVlKSBjb3VudHMgYXMgYWJzZW50IHJhdGhlciB0aGFuIGFzIGEgdG9rZW4gdGhhdCBmYWlscyBhdFxuICogdGhlIGZpcnN0IERhdGFicmlja3MgY2FsbC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlcXVlc3RBY2Nlc3NUb2tlbihyZXE6IEhlYWRlckJlYXJpbmcgfCB1bmRlZmluZWQpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICByZXR1cm4gc3RyaW5nLnRyaW1Ub051bGwocmVxPy5oZWFkZXIoQUNDRVNTX1RPS0VOX0hFQURFUikpID8/IHVuZGVmaW5lZDtcbn1cblxuLyoqIFRoZSBmb3J3YXJkZWQgdXNlciBpZCBvbiBgcmVxYCwgb3IgYHVuZGVmaW5lZGAuICovXG5leHBvcnQgZnVuY3Rpb24gcmVxdWVzdFVzZXJJZChyZXE6IEhlYWRlckJlYXJpbmcgfCB1bmRlZmluZWQpOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICByZXR1cm4gc3RyaW5nLnRyaW1Ub051bGwocmVxPy5oZWFkZXIoVVNFUl9JRF9IRUFERVIpKSA/PyB1bmRlZmluZWQ7XG59XG5cbi8qKiBUaGUgZm9yd2FyZGVkIHVzZXIgZW1haWwgb24gYHJlcWAsIG9yIGB1bmRlZmluZWRgLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlcXVlc3RVc2VyRW1haWwocmVxOiBIZWFkZXJCZWFyaW5nIHwgdW5kZWZpbmVkKTogc3RyaW5nIHwgdW5kZWZpbmVkIHtcbiAgcmV0dXJuIHN0cmluZy50cmltVG9OdWxsKHJlcT8uaGVhZGVyKFVTRVJfRU1BSUxfSEVBREVSKSkgPz8gdW5kZWZpbmVkO1xufVxuXG4vKipcbiAqIFdoZXRoZXIgYHJlcWAgc2hvdWxkIHJ1biBpdHMgRGF0YWJyaWNrcyBjYWxscyBhcyB0aGUgYXBwIHNlcnZpY2UgcHJpbmNpcGFsXG4gKiByYXRoZXIgdGhhbiBPQk8uXG4gKlxuICogLSBgXCJzZXJ2aWNlLXByaW5jaXBhbFwiYCAtPiBhbHdheXMgYHRydWVgLlxuICogLSBgXCJ1c2VyXCJgIC0+IGFsd2F5cyBgZmFsc2VgLCBldmVuIHdpdGggbm8gdG9rZW4uIFRoZSBtb2RlIGlzIGFuIGV4cGxpY2l0XG4gKiAgIGFzc2VydGlvbiB0aGF0IGV2ZXJ5IGNhbGxlciBpcyBPQk8tY2FwYWJsZSwgc28gYSBtaXNzaW5nIHRva2VuIGlzIGEgcmVhbFxuICogICBlcnJvciBhbmQgbXVzdCBzdXJmYWNlIGFzIEFwcEtpdCdzIGBBdXRoZW50aWNhdGlvbkVycm9yYCByYXRoZXIgdGhhbiBiZWluZ1xuICogICBxdWlldGx5IGRvd25ncmFkZWQgdG8gc2hhcmVkIGRhdGEgYWNjZXNzLlxuICogLSBgXCJhdXRvXCJgIC0+IGB0cnVlYCBvbmx5IHdoZW4gdGhlIHJlcXVlc3QgY2FycmllcyBubyB1c2FibGUgT0JPIHRva2VuLlxuICpcbiAqIEEgcmVxdWVzdCBBcHBLaXQgd2lsbCBub3QgYWNjZXB0IGZvciBPQk8gYW55d2F5IChubyB0b2tlbikgY2FuIG5ldmVyIGJlXG4gKiBzZXJ2ZWQgYnkgZW50ZXJpbmcgYGFzVXNlcmAsIHNvIGluIGBcImF1dG9cImAgbW9kZSB0aGUgdG9rZW4gY2hlY2sgaXMgdGhlIHdob2xlXG4gKiBkZWNpc2lvbjogaXQgaXMgdGhlIHNhbWUgY29uZGl0aW9uIEFwcEtpdCBpdHNlbGYgYnJhbmNoZXMgb24sIGp1c3QgcmVzb2x2ZWRcbiAqIGJlZm9yZSBpdCBjYW4gdGhyb3cuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB1c2VTZXJ2aWNlUHJpbmNpcGFsKG1vZGU6IElkZW50aXR5TW9kZSwgcmVxPzogSGVhZGVyQmVhcmluZyk6IGJvb2xlYW4ge1xuICBpZiAobW9kZSA9PT0gXCJzZXJ2aWNlLXByaW5jaXBhbFwiKSByZXR1cm4gdHJ1ZTtcbiAgaWYgKG1vZGUgPT09IFwidXNlclwiKSByZXR1cm4gZmFsc2U7XG4gIHJldHVybiByZXF1ZXN0QWNjZXNzVG9rZW4ocmVxKSA9PT0gdW5kZWZpbmVkO1xufVxuIl19
|
package/lib/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/appkit.ts","../src/config.ts","../src/create-app.ts","../src/databricks.ts","../src/lakebase-resolver.ts","../src/pgaddress.ts","../src/plugin.ts","../src/provision.ts","../index.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["../src/appkit.ts","../src/config.ts","../src/create-app.ts","../src/databricks.ts","../src/identity.ts","../src/lakebase-resolver.ts","../src/pgaddress.ts","../src/plugin.ts","../src/provision.ts","../index.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@databricks/sdk-experimental": "^0.17.0",
|
|
31
|
-
"@dbx-tools/core": "0.6.
|
|
32
|
-
"@dbx-tools/shared-core": "0.6.
|
|
31
|
+
"@dbx-tools/core": "0.6.49",
|
|
32
|
+
"@dbx-tools/shared-core": "0.6.49",
|
|
33
33
|
"yaml": "^2.9.0",
|
|
34
34
|
"zod": "4.3.6"
|
|
35
35
|
},
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"./package.json": "./package.json"
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
|
-
"version": "0.6.
|
|
50
|
+
"version": "0.6.49",
|
|
51
51
|
"types": "./lib/index.d.ts",
|
|
52
52
|
"type": "module",
|
|
53
53
|
"exports": {
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Databricks identity a request's workspace calls run as.
|
|
3
|
+
*
|
|
4
|
+
* AppKit gives a plugin two identities: the ambient SERVICE context (the app's
|
|
5
|
+
* own service principal) and a per-request USER context entered with
|
|
6
|
+
* `asUser(req)`, which authenticates as the caller on-behalf-of (OBO). The
|
|
7
|
+
* choice is one call, so the whole decision is "do we enter `asUser` for this
|
|
8
|
+
* request".
|
|
9
|
+
*
|
|
10
|
+
* `asUser(req)` needs the OBO token the platform front door forwards on
|
|
11
|
+
* {@link ACCESS_TOKEN_HEADER}. Measured against the installed AppKit, its
|
|
12
|
+
* behavior when that header is absent depends ENTIRELY on `NODE_ENV`:
|
|
13
|
+
*
|
|
14
|
+
* | `NODE_ENV` | no `x-forwarded-access-token` |
|
|
15
|
+
* | ------------- | ---------------------------------------------------- |
|
|
16
|
+
* | `development` | logs a warning, silently runs as the service principal |
|
|
17
|
+
* | anything else | throws `AuthenticationError: Missing user token` |
|
|
18
|
+
*
|
|
19
|
+
* That production throw is correct for an app behind the Databricks front door,
|
|
20
|
+
* where a missing token means something is wrong. It is fatal for an app whose
|
|
21
|
+
* traffic legitimately arrives WITHOUT one:
|
|
22
|
+
*
|
|
23
|
+
* - a public tunnel (`@dbx-tools/cli-tunnel`), where callers authenticate by
|
|
24
|
+
* email OTP and no OBO token exists to forward - the gate can prove WHO the
|
|
25
|
+
* caller is, but it cannot mint a Databricks credential for them;
|
|
26
|
+
* - any reverse proxy, webhook, or bot channel (`POST /api/teams/messages`)
|
|
27
|
+
* that authenticates its own way.
|
|
28
|
+
*
|
|
29
|
+
* Such an app must not run with `NODE_ENV=development` just to get the fallback:
|
|
30
|
+
* that flag also relaxes secure cookies, AppKit's own dev affordances, and
|
|
31
|
+
* `allowUnauthenticated` escape hatches. Hence {@link IdentityMode}:
|
|
32
|
+
*
|
|
33
|
+
* - `"user"` - always OBO. Per-user attribution and per-user Genie / Unity
|
|
34
|
+
* Catalog row filters. Correct when every caller is a workspace member.
|
|
35
|
+
* - `"service-principal"` - always the app's own identity. Needs no OBO
|
|
36
|
+
* scopes and works for any caller, at the cost of per-user data scoping.
|
|
37
|
+
* - `"auto"` - OBO when the request actually carries a usable OBO token,
|
|
38
|
+
* the service principal otherwise. One deployment then serves BOTH doors
|
|
39
|
+
* correctly: front-door requests keep full per-user scoping, while tunnel /
|
|
40
|
+
* webhook requests degrade to the service principal instead of 500ing.
|
|
41
|
+
*
|
|
42
|
+
* `"auto"` decides per REQUEST, not per boot, because a single container serves
|
|
43
|
+
* both doors at once - the tunnel gate and the platform front door share a port
|
|
44
|
+
* (see `@dbx-tools/cli-tunnel`). A boot-time flag would have to be wrong for one
|
|
45
|
+
* of them.
|
|
46
|
+
*
|
|
47
|
+
* What the service principal does NOT change is WHO the request belongs to. The
|
|
48
|
+
* caller's identity still arrives on {@link USER_ID_HEADER} /
|
|
49
|
+
* {@link USER_EMAIL_HEADER}, so memory threads, cache namespaces, and trace
|
|
50
|
+
* attribution stay per-user. Only the Databricks credential is shared.
|
|
51
|
+
*
|
|
52
|
+
* @module
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import { ConfigurationError } from "@databricks/appkit";
|
|
56
|
+
import { env, string, token, type EnvKey } from "@dbx-tools/shared-core";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Header the Databricks Apps front door forwards the caller's OBO token on.
|
|
60
|
+
* Its presence is what makes `asUser(req)` viable, so it is the signal
|
|
61
|
+
* {@link useServicePrincipal} reads in `"auto"` mode.
|
|
62
|
+
*
|
|
63
|
+
* Re-exported from `@dbx-tools/shared-core`'s `token` module (which reads the
|
|
64
|
+
* same header to decode OAuth scopes) rather than re-spelled, so the name for
|
|
65
|
+
* this wire contract exists once.
|
|
66
|
+
*/
|
|
67
|
+
export const ACCESS_TOKEN_HEADER = token.ACCESS_TOKEN_HEADER;
|
|
68
|
+
|
|
69
|
+
/** Header the Databricks Apps front door forwards the caller's user id on. */
|
|
70
|
+
export const USER_ID_HEADER = token.USER_ID_HEADER;
|
|
71
|
+
|
|
72
|
+
/** Header the Databricks Apps front door forwards the caller's email on. */
|
|
73
|
+
export const USER_EMAIL_HEADER = token.USER_EMAIL_HEADER;
|
|
74
|
+
|
|
75
|
+
/** Identity a request's Databricks calls run as. See the module docs. */
|
|
76
|
+
export type IdentityMode = "user" | "service-principal" | "auto";
|
|
77
|
+
|
|
78
|
+
/** Every accepted {@link IdentityMode}, in the order docs and schemas list them. */
|
|
79
|
+
export const IDENTITY_MODES: readonly IdentityMode[] = ["user", "service-principal", "auto"];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Default mode. `"user"` keeps OBO the only identity unless an app opts in, so
|
|
83
|
+
* adopting this option can never silently widen an existing app's data access.
|
|
84
|
+
*/
|
|
85
|
+
export const DEFAULT_IDENTITY_MODE: IdentityMode = "user";
|
|
86
|
+
|
|
87
|
+
/** The subset of `express.Request` this module reads - one header lookup. */
|
|
88
|
+
export interface HeaderBearing {
|
|
89
|
+
header(name: string): string | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a configured mode: explicit config value, then the first non-empty
|
|
94
|
+
* variable among `envKeys`, then {@link DEFAULT_IDENTITY_MODE}.
|
|
95
|
+
*
|
|
96
|
+
* An unrecognized value throws rather than falling back. Falling back would
|
|
97
|
+
* silently keep serving OBO - and the production `AuthenticationError` it
|
|
98
|
+
* produces - to exactly the callers the option was set to accommodate, and a
|
|
99
|
+
* typo (`"obo"`, `"sp"`) is the likeliest way to get one.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveIdentityMode(
|
|
102
|
+
configured: string | undefined,
|
|
103
|
+
envKeys: EnvKey,
|
|
104
|
+
field = "identity",
|
|
105
|
+
): IdentityMode {
|
|
106
|
+
const raw = env.string(configured, envKeys);
|
|
107
|
+
if (raw === null) return DEFAULT_IDENTITY_MODE;
|
|
108
|
+
const mode = raw.toLowerCase() as IdentityMode;
|
|
109
|
+
if (!IDENTITY_MODES.includes(mode)) {
|
|
110
|
+
const names = typeof envKeys === "string" ? envKeys : envKeys.join(" / ");
|
|
111
|
+
throw new ConfigurationError(
|
|
112
|
+
`${field} must be one of ${IDENTITY_MODES.join(" | ")} (env: ${names})`,
|
|
113
|
+
{ context: { field, envVar: names, received: raw } },
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return mode;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The OBO token on `req`, or `undefined`. Read through the same trim as every
|
|
121
|
+
* other header, so a header present-but-blank (which some proxies emit for an
|
|
122
|
+
* unset upstream value) counts as absent rather than as a token that fails at
|
|
123
|
+
* the first Databricks call.
|
|
124
|
+
*/
|
|
125
|
+
export function requestAccessToken(req: HeaderBearing | undefined): string | undefined {
|
|
126
|
+
return string.trimToNull(req?.header(ACCESS_TOKEN_HEADER)) ?? undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The forwarded user id on `req`, or `undefined`. */
|
|
130
|
+
export function requestUserId(req: HeaderBearing | undefined): string | undefined {
|
|
131
|
+
return string.trimToNull(req?.header(USER_ID_HEADER)) ?? undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The forwarded user email on `req`, or `undefined`. */
|
|
135
|
+
export function requestUserEmail(req: HeaderBearing | undefined): string | undefined {
|
|
136
|
+
return string.trimToNull(req?.header(USER_EMAIL_HEADER)) ?? undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether `req` should run its Databricks calls as the app service principal
|
|
141
|
+
* rather than OBO.
|
|
142
|
+
*
|
|
143
|
+
* - `"service-principal"` -> always `true`.
|
|
144
|
+
* - `"user"` -> always `false`, even with no token. The mode is an explicit
|
|
145
|
+
* assertion that every caller is OBO-capable, so a missing token is a real
|
|
146
|
+
* error and must surface as AppKit's `AuthenticationError` rather than being
|
|
147
|
+
* quietly downgraded to shared data access.
|
|
148
|
+
* - `"auto"` -> `true` only when the request carries no usable OBO token.
|
|
149
|
+
*
|
|
150
|
+
* A request AppKit will not accept for OBO anyway (no token) can never be
|
|
151
|
+
* served by entering `asUser`, so in `"auto"` mode the token check is the whole
|
|
152
|
+
* decision: it is the same condition AppKit itself branches on, just resolved
|
|
153
|
+
* before it can throw.
|
|
154
|
+
*/
|
|
155
|
+
export function useServicePrincipal(mode: IdentityMode, req?: HeaderBearing): boolean {
|
|
156
|
+
if (mode === "service-principal") return true;
|
|
157
|
+
if (mode === "user") return false;
|
|
158
|
+
return requestAccessToken(req) === undefined;
|
|
159
|
+
}
|