@databricks/appkit 0.35.0 → 0.35.2
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 +3 -3
- package/dist/appkit/package.js +1 -1
- package/dist/connectors/files/client.js +28 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/files/index.js +1 -1
- package/dist/connectors/index.js +1 -1
- package/dist/plugins/files/plugin.d.ts +216 -20
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +618 -191
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/policy.d.ts +30 -1
- package/dist/plugins/files/policy.d.ts.map +1 -1
- package/dist/plugins/files/policy.js.map +1 -1
- package/dist/plugins/files/types.d.ts +136 -5
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/docs/api/appkit/Interface.FilePolicyUser.md +15 -1
- package/docs/development/llm-guide.md +0 -1
- package/docs/plugins/files.md +199 -19
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/files/types.ts"],"mappings":";;;;;;;;;UAOiB,YAAA;EAAY;EAE3B,aAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/files/types.ts"],"mappings":";;;;;;;;;UAOiB,YAAA;EAAY;EAE3B,aAAA;EAamB;;;;;EAPnB,WAAA;EAOS;EALT,kBAAA,GAAqB,MAAA;EAoDjB;;AAoBN;;EAnEE,MAAA,GAAS,UAAA;EAoE6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EArBtC,IAAA;AAAA;;;;;;;;;;AAuCF;;;;;;;;UAnBiB,SAAA;EACf,IAAA,CAAK,aAAA,YAAyB,OAAA,CAAQ,cAAA;EACtC,IAAA,CAAK,QAAA,UAAkB,OAAA;IAAY,OAAA;EAAA,IAAqB,OAAA;EACxD,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,gBAAA;EACpC,MAAA,CAAO,QAAA,WAAmB,OAAA;EAC1B,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,YAAA;EACpC,MAAA,CACE,QAAA,UACA,QAAA,EAAU,cAAA,GAAiB,MAAA,WAC3B,OAAA;IAAY,SAAA;EAAA,IACX,OAAA;EACH,eAAA,CAAgB,aAAA,WAAwB,OAAA;EACxC,MAAA,CAAO,QAAA,WAAmB,OAAA;EAC1B,OAAA,CAAQ,QAAA,WAAmB,OAAA,CAAQ,WAAA;AAAA;;;;UAMpB,YAAA,SAAqB,gBAAA;EAwDV;EAtD1B,OAAA;EAsD6B;EApD7B,OAAA,GAAU,MAAA,SAAe,YAAA;EAyDV;EAvDf,kBAAA,GAAqB,MAAA;;EAErB,aAAA;EAuDA;;;;EAlDA,WAAA;EA4De;;;;;;;;;;AAyCjB;;;;;;;;;;;;;;;AAqBA;;;;;;;;;;EAtFE,IAAA;AAAA;;KAIU,cAAA,GAAiB,KAAA,CAAM,cAAA;;KAGvB,gBAAA,GAAmB,KAAA,CAAM,gBAAA;;;;UAKpB,YAAA;;EAEf,aAAA;;EAEA,WAAA;;EAEA,YAAA;AAAA;;;;UAMe,WAAA,SAAoB,YAAA;;EAEnC,WAAA;;EAEA,MAAA;;EAEA,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAmCU,YAAA,GAAe,SAAA;EACzB,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,SAAA;AAAA;;;;;;;;;;;;;;;;;;UAoBf,WAAA;EAAA,CACd,SAAA,WAAoB,YAAA;EACrB,MAAA,GAAS,SAAA,aAAsB,YAAA;AAAA"}
|
|
@@ -11,6 +11,8 @@ id: string;
|
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
Identifier of the requesting caller. For end-user HTTP requests this is the value of the `x-forwarded-user` header; for direct SDK calls and header-less HTTP requests (which run as the service principal), this is the service principal's ID.
|
|
15
|
+
|
|
14
16
|
***
|
|
15
17
|
|
|
16
18
|
### isServicePrincipal?[](#isserviceprincipal "Direct link to isServicePrincipal?")
|
|
@@ -20,4 +22,16 @@ optional isServicePrincipal: boolean;
|
|
|
20
22
|
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
`true` when the
|
|
25
|
+
`true` when the call is executing as the service principal — either a direct SDK call (`appKit.files(...)`) or an HTTP request that arrived without an `x-forwarded-user` / `x-forwarded-access-token` header. Policy authors typically check this first to distinguish SP traffic from end-user traffic.
|
|
26
|
+
|
|
27
|
+
The flag reflects the **policy user** the plugin selects, which combines the volume's effective `auth` mode with the headers on the incoming request. The full matrix:
|
|
28
|
+
|
|
29
|
+
| Volume `auth` | Path | Headers | `isServicePrincipal` | Notes |
|
|
30
|
+
| ------------------- | -------------------------- | --------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
|
|
31
|
+
| `service-principal` | HTTP | `x-forwarded-user` present | `false` (or unset) | Pre-OBO behavior. Policy sees the end user but the SDK call still runs as the SP. |
|
|
32
|
+
| `service-principal` | HTTP | no `x-forwarded-user` | `true` | Headerless request — policy and SDK both run as the SP. |
|
|
33
|
+
| `on-behalf-of-user` | HTTP | valid token + user header | `false` | Real end-user execution. Policy sees the user; the SDK call also runs as the user. |
|
|
34
|
+
| `on-behalf-of-user` | HTTP | missing token, dev-fallback | `true` | Only reachable when `NODE_ENV === "development"` (prod returns 401). Treated as SP traffic. |
|
|
35
|
+
| any | Programmatic `asUser(req)` | `x-forwarded-user` present | `false` | `asUser` extracts the user; the SDK call runs as the user inside `runInUserContext`. |
|
|
36
|
+
|
|
37
|
+
Programmatic calls without `asUser(req)` always set `isServicePrincipal: true` because no request is available to derive a user identity from. OBO volume defaults apply only to HTTP route traffic; for programmatic per-user execution, use `asUser(req)`.
|
|
@@ -24,7 +24,6 @@ This guide is designed to work even when you *do not* have access to the AppKit
|
|
|
24
24
|
|
|
25
25
|
* **Do not invent APIs**. If unsure, stick to the patterns shown in the documentation and only use documented exports from `@databricks/appkit` and `@databricks/appkit-ui`.
|
|
26
26
|
* **`createApp()` is async**. Prefer **top-level `await createApp(...)`**. If you can't, use `void createApp(...)` and do not ignore promise rejection.
|
|
27
|
-
* **Always memoize query parameters** passed to `useAnalyticsQuery` / charts to avoid refetch loops.
|
|
28
27
|
* **Always handle loading/error/empty states** in UI (use `Skeleton`, error text, empty state).
|
|
29
28
|
* **Always use `sql.*` helpers** for query parameters (do not pass raw strings/numbers unless the query expects none).
|
|
30
29
|
* **Never construct SQL strings dynamically**. Use parameterized queries with `:paramName`.
|
package/docs/plugins/files.md
CHANGED
|
@@ -11,7 +11,7 @@ File operations against Databricks Unity Catalog Volumes. Supports listing, read
|
|
|
11
11
|
* Upload size limits with streaming enforcement
|
|
12
12
|
* Automatic cache invalidation on write operations
|
|
13
13
|
* Custom content type mappings
|
|
14
|
-
* Per-
|
|
14
|
+
* **Per-volume auth modes**: each volume can run as the service principal (the default) or on behalf of the end user
|
|
15
15
|
* **Access policies**: Per-volume policy functions that gate read and write operations
|
|
16
16
|
|
|
17
17
|
## Basic usage[](#basic-usage "Direct link to Basic usage")
|
|
@@ -73,6 +73,11 @@ interface IFilesConfig {
|
|
|
73
73
|
customContentTypes?: Record<string, string>;
|
|
74
74
|
/** Maximum upload size in bytes. Defaults to 5 GB. Inherited by all volumes. */
|
|
75
75
|
maxUploadSize?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Plugin-level default auth mode. Volumes inherit this when they do not
|
|
78
|
+
* set `VolumeConfig.auth`. Defaults to `"service-principal"`.
|
|
79
|
+
*/
|
|
80
|
+
auth?: "service-principal" | "on-behalf-of-user";
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
interface VolumeConfig {
|
|
@@ -82,6 +87,11 @@ interface VolumeConfig {
|
|
|
82
87
|
maxUploadSize?: number;
|
|
83
88
|
/** Map of file extensions to MIME types for this volume. Overrides plugin-level default. */
|
|
84
89
|
customContentTypes?: Record<string, string>;
|
|
90
|
+
/**
|
|
91
|
+
* Per-volume auth mode. Inherits from `IFilesConfig.auth` when not set;
|
|
92
|
+
* defaults to `"service-principal"`.
|
|
93
|
+
*/
|
|
94
|
+
auth?: "service-principal" | "on-behalf-of-user";
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
```
|
|
@@ -102,6 +112,80 @@ files({
|
|
|
102
112
|
|
|
103
113
|
```
|
|
104
114
|
|
|
115
|
+
### Auth modes[](#auth-modes "Direct link to Auth modes")
|
|
116
|
+
|
|
117
|
+
Each volume runs in one of two auth modes. The mode determines which identity executes the underlying Unity Catalog SDK call — and therefore which UC grant applies:
|
|
118
|
+
|
|
119
|
+
| Mode | SDK identity | Required UC grant on the volume |
|
|
120
|
+
| ------------------------------- | ----------------------------- | ------------------------------------------------------- |
|
|
121
|
+
| `"service-principal"` (default) | the app's service principal | `WRITE_VOLUME` (or read-equivalent) on the **SP** |
|
|
122
|
+
| `"on-behalf-of-user"` | the end user from the request | `WRITE_VOLUME` (or read-equivalent) on the **end user** |
|
|
123
|
+
|
|
124
|
+
#### Resolution order[](#resolution-order "Direct link to Resolution order")
|
|
125
|
+
|
|
126
|
+
For each volume the plugin resolves the auth mode in this order:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
VolumeConfig.auth > IFilesConfig.auth > "service-principal"
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Set `IFilesConfig.auth` to flip the default for every volume in one place, and override individual volumes via `VolumeConfig.auth`.
|
|
134
|
+
|
|
135
|
+
#### Service-principal mode (default)[](#service-principal-mode-default "Direct link to Service-principal mode (default)")
|
|
136
|
+
|
|
137
|
+
Every HTTP request executes as the app's service principal. The end-user identity (from `x-forwarded-user`) is still passed into the volume policy, but the SDK call uses the SP's credentials:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
files({
|
|
141
|
+
volumes: {
|
|
142
|
+
exports: {
|
|
143
|
+
// auth is implicit: "service-principal"
|
|
144
|
+
policy: files.policy.publicRead(),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use SP mode for shared resources, app-managed exports, or any case where you want a single grant on the SP to govern all access.
|
|
152
|
+
|
|
153
|
+
#### On-behalf-of-user mode[](#on-behalf-of-user-mode "Direct link to On-behalf-of-user mode")
|
|
154
|
+
|
|
155
|
+
Every HTTP request executes as the end user. The plugin pulls the user identity and access token from the headers Databricks Apps inject (`x-forwarded-user` and `x-forwarded-access-token`) and runs the SDK call inside `runInUserContext`:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
files({
|
|
159
|
+
volumes: {
|
|
160
|
+
"user-uploads": {
|
|
161
|
+
auth: "on-behalf-of-user",
|
|
162
|
+
// The policy sees the real end user (isServicePrincipal: false).
|
|
163
|
+
// You can use the volume policy in addition to UC grants.
|
|
164
|
+
policy: (action, _resource, user) =>
|
|
165
|
+
// Only allow real end users, never the SP.
|
|
166
|
+
!user.isServicePrincipal,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use OBO mode when the per-user UC grant is meaningful — e.g. enforcement of UC ACLs at the SDK layer, or audit trails that need to attribute the API call to the end user instead of the app's SP.
|
|
174
|
+
|
|
175
|
+
#### Production vs development behavior[](#production-vs-development-behavior "Direct link to Production vs development behavior")
|
|
176
|
+
|
|
177
|
+
| Environment | OBO request with **valid** token | OBO request with **missing** `x-forwarded-access-token` |
|
|
178
|
+
| ------------------------------------------ | -------------------------------- | ------------------------------------------------------- |
|
|
179
|
+
| Production | Runs as the end user. | `401 Unauthorized` — no SDK call is made. |
|
|
180
|
+
| Development (`NODE_ENV === "development"`) | Runs as the end user. | Logs a warning, falls back to the SP, and continues. |
|
|
181
|
+
|
|
182
|
+
The dev-mode fallback exists so local testing without a Databricks Apps reverse proxy continues to work; deployed apps always have the headers injected.
|
|
183
|
+
|
|
184
|
+
#### Limitations[](#limitations "Direct link to Limitations")
|
|
185
|
+
|
|
186
|
+
* The plugin manifest's `getResourceRequirements()` declares `WRITE_VOLUME` on the **service principal** for every volume, regardless of the volume's `auth` mode. For OBO volumes, the actual permission requirement is on the **end user** — communicate this out-of-band (deployment runbooks, customer onboarding docs) until the plugin manifest schema gains a per-volume auth scope field.
|
|
187
|
+
* OBO volumes disable the read/list cache entirely. The cache layer keys by `getCurrentUserId()`, so a write by user A wouldn't invalidate user B's view of the same path; rather than risk cross-user staleness, OBO traffic skips the cache and fetches fresh on every request. SP volumes still cache (single slice keyed by the SP id).
|
|
188
|
+
|
|
105
189
|
### Permission model[](#permission-model "Direct link to Permission model")
|
|
106
190
|
|
|
107
191
|
There are three layers of access control in the files plugin. Understanding how they interact is critical for securing your app:
|
|
@@ -109,11 +193,14 @@ There are three layers of access control in the files plugin. Understanding how
|
|
|
109
193
|
```text
|
|
110
194
|
┌─────────────────────────────────────────────────┐
|
|
111
195
|
│ Unity Catalog grants │
|
|
112
|
-
│
|
|
196
|
+
│ WRITE_VOLUME on the SP (auth: service-principal)│
|
|
197
|
+
│ WRITE_VOLUME on the user (auth: on-behalf-of-user)│
|
|
113
198
|
├─────────────────────────────────────────────────┤
|
|
114
199
|
│ Execution identity │
|
|
115
|
-
│
|
|
116
|
-
│
|
|
200
|
+
│ Resolved per volume from VolumeConfig.auth ?? │
|
|
201
|
+
│ IFilesConfig.auth ?? "service-principal". │
|
|
202
|
+
│ asUser(req) is a hard override at the SDK │
|
|
203
|
+
│ level for the programmatic API. │
|
|
117
204
|
├─────────────────────────────────────────────────┤
|
|
118
205
|
│ File policies │
|
|
119
206
|
│ Per-volume (action, resource, user) → boolean │
|
|
@@ -122,13 +209,15 @@ There are three layers of access control in the files plugin. Understanding how
|
|
|
122
209
|
|
|
123
210
|
```
|
|
124
211
|
|
|
125
|
-
* **UC grants** control what
|
|
126
|
-
* **Execution identity** determines whose credentials are used for the actual API call.
|
|
127
|
-
* **File policies** are application-level checks evaluated **before** the API call. They receive the
|
|
212
|
+
* **UC grants** control what an identity can do at the Databricks level. Which identity needs the grant depends on the volume's auth mode (see [Auth modes](#auth-modes)). For SP volumes, the SP needs `WRITE_VOLUME` (the plugin declares this in its manifest). For OBO volumes, the **end user** needs `WRITE_VOLUME` on the volume; the SP itself does not.
|
|
213
|
+
* **Execution identity** determines whose credentials are used for the actual API call. Each volume resolves to either the service principal or the end user, per its `auth` setting. The programmatic API also exposes `asUser(req)` to force per-user execution regardless of the volume's `auth`.
|
|
214
|
+
* **File policies** are application-level checks evaluated **before** the API call. They receive a `FilePolicyUser` describing the caller and decide allow/deny. On HTTP routes the policy user is selected based on the volume's `auth` mode and the request headers — see the [`isServicePrincipal` matrix](#policy-user-matrix). On SP volumes when `x-forwarded-user` is absent, the policy receives `{ id: <sp-id>, isServicePrincipal: true }` and decides whether to allow service-principal traffic. This is the only gate that distinguishes between users on HTTP routes.
|
|
128
215
|
|
|
129
216
|
warning
|
|
130
217
|
|
|
131
|
-
|
|
218
|
+
For service-principal volumes, every HTTP request executes as the SP regardless of which user made it — so removing a user's UC `WRITE_VOLUME` grant has **no effect** on HTTP access. Policies are how you restrict what individual users can do through your app.
|
|
219
|
+
|
|
220
|
+
For on-behalf-of-user volumes, requests execute as the requesting user — so each user must have `WRITE_VOLUME` on the volume themselves, and you can rely on UC grants in addition to policies.
|
|
132
221
|
|
|
133
222
|
New in v0.21.0
|
|
134
223
|
|
|
@@ -211,6 +300,58 @@ files({
|
|
|
211
300
|
|
|
212
301
|
```
|
|
213
302
|
|
|
303
|
+
#### OBO policy example[](#obo-policy-example "Direct link to OBO policy example")
|
|
304
|
+
|
|
305
|
+
For on-behalf-of-user volumes, the policy receives `isServicePrincipal: false` whenever the request runs with a real end-user identity. A common pattern is to deny SP traffic outright so anonymous (header-less) calls can't reach the volume:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { type FilePolicy } from "@databricks/appkit";
|
|
309
|
+
|
|
310
|
+
// Deny anything running as the service principal — including the dev-mode
|
|
311
|
+
// fallback when no x-forwarded-access-token was provided. Real end users
|
|
312
|
+
// (with isServicePrincipal: false) get the configured access.
|
|
313
|
+
const usersOnly: FilePolicy = (_action, _resource, user) => {
|
|
314
|
+
return user.isServicePrincipal !== true;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
files({
|
|
318
|
+
volumes: {
|
|
319
|
+
"user-uploads": {
|
|
320
|
+
auth: "on-behalf-of-user",
|
|
321
|
+
policy: usersOnly,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
You can compose it with any other policy via `files.policy.all(...)` to add per-action gating:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
files({
|
|
332
|
+
volumes: {
|
|
333
|
+
"user-uploads": {
|
|
334
|
+
auth: "on-behalf-of-user",
|
|
335
|
+
policy: files.policy.all(usersOnly, files.policy.publicRead()),
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### Policy user matrix[](#policy-user-matrix "Direct link to Policy user matrix")
|
|
343
|
+
|
|
344
|
+
The plugin selects the policy user based on the volume's effective `auth` mode and the request headers. The full table:
|
|
345
|
+
|
|
346
|
+
| Volume `auth` | Path | Headers | `isServicePrincipal` | Notes |
|
|
347
|
+
| ------------------- | -------------------------- | --------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
|
|
348
|
+
| `service-principal` | HTTP | `x-forwarded-user` present | `false` (or unset) | Pre-OBO behavior. Policy sees the end user but the SDK call still runs as the SP. |
|
|
349
|
+
| `service-principal` | HTTP | no `x-forwarded-user` | `true` | Headerless request — policy and SDK both run as the SP. |
|
|
350
|
+
| `on-behalf-of-user` | HTTP | valid token + user header | `false` | Real end-user execution. Policy sees the user; the SDK call also runs as the user. |
|
|
351
|
+
| `on-behalf-of-user` | HTTP | missing token, dev-fallback | `true` | Only reachable when `NODE_ENV === "development"` (prod returns 401). Treated as SP traffic. |
|
|
352
|
+
| any | Programmatic `asUser(req)` | `x-forwarded-user` present | `false` | `asUser` extracts the user; the SDK call runs as the user inside `runInUserContext`. |
|
|
353
|
+
| any | Programmatic (no `asUser`) | n/a | `true` | No request available to derive a user — runs as the SP. |
|
|
354
|
+
|
|
214
355
|
#### Enforcement[](#enforcement "Direct link to Enforcement")
|
|
215
356
|
|
|
216
357
|
* **HTTP routes**: Policy checked before every operation. Denied → `403` JSON response with `Policy denied "{action}" on volume "{volumeKey}"`.
|
|
@@ -236,7 +377,7 @@ Dangerous MIME types (`text/html`, `text/javascript`, `application/javascript`,
|
|
|
236
377
|
|
|
237
378
|
## HTTP routes[](#http-routes "Direct link to HTTP routes")
|
|
238
379
|
|
|
239
|
-
Routes are mounted at `/api/files/*`.
|
|
380
|
+
Routes are mounted at `/api/files/*`. Each route resolves the volume's [auth mode](#auth-modes) and either executes as the service principal (the default) or wraps the SDK call in `runInUserContext` for OBO volumes. Before every operation the volume policy runs against the resolved policy user — see the [policy user matrix](#policy-user-matrix) for the exact mapping. See also [Access policies](#access-policies).
|
|
240
381
|
|
|
241
382
|
| Method | Path | Query / Body | Response |
|
|
242
383
|
| ------ | ---------------------- | ---------------------------- | ------------------------------------------------------------ |
|
|
@@ -292,22 +433,37 @@ Write operations (`upload`, `mkdir`, `delete`) automatically invalidate the cach
|
|
|
292
433
|
|
|
293
434
|
## Programmatic API[](#programmatic-api "Direct link to Programmatic API")
|
|
294
435
|
|
|
295
|
-
The `
|
|
436
|
+
The `files` plugin export is a callable that accepts a volume key and returns a `VolumeHandle`. The handle exposes all `VolumeAPI` methods directly and an `asUser(req)` method for opting into per-user execution.
|
|
296
437
|
|
|
297
438
|
```ts
|
|
298
|
-
//
|
|
439
|
+
// Default — runs as the service principal, regardless of the volume's auth
|
|
440
|
+
// setting (no req is available to derive a user from).
|
|
441
|
+
const entries = await appkit.files("uploads").list();
|
|
442
|
+
|
|
443
|
+
// asUser(req) — runs as the end user, regardless of the volume's auth
|
|
444
|
+
// setting. Forces SDK calls into runInUserContext using the request's
|
|
445
|
+
// x-forwarded-user / x-forwarded-access-token headers.
|
|
299
446
|
const entries = await appkit.files("uploads").asUser(req).list();
|
|
300
447
|
const content = await appkit.files("exports").asUser(req).read("report.csv");
|
|
301
448
|
|
|
302
|
-
// Service principal access (logs a warning encouraging OBO)
|
|
303
|
-
const entries = await appkit.files("uploads").list();
|
|
304
|
-
|
|
305
449
|
// Named accessor
|
|
306
450
|
const vol = appkit.files.volume("uploads");
|
|
307
451
|
await vol.asUser(req).list();
|
|
308
452
|
|
|
309
453
|
```
|
|
310
454
|
|
|
455
|
+
### `asUser(req)`[](#asuserreq "Direct link to asuserreq")
|
|
456
|
+
|
|
457
|
+
`asUser(req)` is the supported path for programmatic per-user execution. The returned API runs every method inside `runInUserContext` so the underlying `WorkspaceClient` is the user-token client — the SDK call executes as the user, not just the policy check.
|
|
458
|
+
|
|
459
|
+
In production, `asUser(req)` throws `AuthenticationError.missingToken` when either `x-forwarded-user` or `x-forwarded-access-token` is absent — both headers are required to mint a user-scoped client. In development (`NODE_ENV === "development"`) it logs a warning and falls back to the service principal so local testing without a Databricks Apps reverse proxy keeps working — the fallback skips the `runInUserContext` wrap.
|
|
460
|
+
|
|
461
|
+
Programmatic OBO without `asUser(req)`
|
|
462
|
+
|
|
463
|
+
A volume configured with `auth: "on-behalf-of-user"` only routes through `runInUserContext` on the **HTTP route path**, where the request headers are available. A direct programmatic call — `appkit.files("obo-vol").list()` — has no request to derive an end-user identity from, so it executes against whatever client `getWorkspaceClient()` resolves to at the call site (typically the SP at the top level).
|
|
464
|
+
|
|
465
|
+
For programmatic per-user execution, always use `asUser(req)`. The volume's `auth` mode controls HTTP traffic; `asUser(req)` controls programmatic traffic.
|
|
466
|
+
|
|
311
467
|
### VolumeAPI methods[](#volumeapi-methods "Direct link to VolumeAPI methods")
|
|
312
468
|
|
|
313
469
|
| Method | Signature | Returns |
|
|
@@ -375,9 +531,20 @@ interface FileResource {
|
|
|
375
531
|
}
|
|
376
532
|
|
|
377
533
|
interface FilePolicyUser {
|
|
378
|
-
/**
|
|
534
|
+
/**
|
|
535
|
+
* Identifier of the requesting caller. For end-user HTTP requests this is
|
|
536
|
+
* the value of the `x-forwarded-user` header; for direct SDK calls and
|
|
537
|
+
* header-less HTTP requests (which run as the service principal), this
|
|
538
|
+
* is the service principal's ID.
|
|
539
|
+
*/
|
|
379
540
|
id: string;
|
|
380
|
-
/**
|
|
541
|
+
/**
|
|
542
|
+
* `true` when the call is executing as the service principal — either a
|
|
543
|
+
* direct SDK call (`appKit.files(...)` without `asUser`), an HTTP request
|
|
544
|
+
* with no forwarded headers, or the dev-mode fallback for an OBO volume
|
|
545
|
+
* with a missing token. See the [policy user matrix](#policy-user-matrix)
|
|
546
|
+
* for the full table.
|
|
547
|
+
*/
|
|
381
548
|
isServicePrincipal?: boolean;
|
|
382
549
|
}
|
|
383
550
|
|
|
@@ -394,6 +561,11 @@ interface VolumeConfig {
|
|
|
394
561
|
maxUploadSize?: number;
|
|
395
562
|
/** Map of file extensions to MIME types for this volume. */
|
|
396
563
|
customContentTypes?: Record<string, string>;
|
|
564
|
+
/**
|
|
565
|
+
* Per-volume auth mode. Inherits from `IFilesConfig.auth` when not set;
|
|
566
|
+
* defaults to `"service-principal"`.
|
|
567
|
+
*/
|
|
568
|
+
auth?: "service-principal" | "on-behalf-of-user";
|
|
397
569
|
}
|
|
398
570
|
|
|
399
571
|
interface VolumeAPI {
|
|
@@ -408,7 +580,10 @@ interface VolumeAPI {
|
|
|
408
580
|
preview(filePath: string): Promise<FilePreview>;
|
|
409
581
|
}
|
|
410
582
|
|
|
411
|
-
/**
|
|
583
|
+
/**
|
|
584
|
+
* Volume handle: all VolumeAPI methods (run as the service principal by
|
|
585
|
+
* default) + asUser() to force per-user execution at the SDK level.
|
|
586
|
+
*/
|
|
412
587
|
type VolumeHandle = VolumeAPI & {
|
|
413
588
|
asUser: (req: Request) => VolumeAPI;
|
|
414
589
|
};
|
|
@@ -427,9 +602,12 @@ Built-in extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`, `
|
|
|
427
602
|
|
|
428
603
|
## User context[](#user-context "Direct link to User context")
|
|
429
604
|
|
|
430
|
-
HTTP routes
|
|
605
|
+
HTTP routes execute as either the service principal or the end user, depending on the volume's [auth mode](#auth-modes):
|
|
431
606
|
|
|
432
|
-
|
|
607
|
+
* **Service-principal volumes** (the default): the SP's Databricks credentials are used for the API call. User identity is extracted from the `x-forwarded-user` header and passed to the volume's [access policy](#access-policies) for authorization, but the SDK call still runs as the SP. When the header is absent the policy is handed `{ id: <sp-id>, isServicePrincipal: true }` and decides whether to allow the call — in practice that branch only fires in development without a reverse proxy or when an upstream proxy is misconfigured, since real Databricks Apps runtimes always forward the header. UC grants on the **SP** determine what operations are possible.
|
|
608
|
+
* **On-behalf-of-user volumes**: the end user's access token (from `x-forwarded-access-token`) is used to mint the SDK client, so the API call runs with the user's identity. Both the policy and the SDK see the user. UC grants on the **end user** determine what operations are possible. In production, requests with a missing token return `401`; in development (`NODE_ENV === "development"`) they fall back to the SP with a warning.
|
|
609
|
+
|
|
610
|
+
The programmatic API returns a `VolumeHandle` that exposes all `VolumeAPI` methods directly and an `asUser(req)` method for forcing per-user execution. Calling a method without `asUser()` runs the policy and the SDK call as the SP. `asUser(req)` is a hard override at the SDK level: it forces every subsequent call to execute as the end user inside `runInUserContext`, regardless of the volume's `auth` setting. In production, `asUser(req)` throws `AuthenticationError.missingToken` when either `x-forwarded-user` or `x-forwarded-access-token` is absent — both headers are required. In development it falls back to the service principal instead, so local testing without a reverse proxy continues to work.
|
|
433
611
|
|
|
434
612
|
## Resource requirements[](#resource-requirements "Direct link to Resource requirements")
|
|
435
613
|
|
|
@@ -437,6 +615,8 @@ Volume resources are declared **dynamically** via `getResourceRequirements(confi
|
|
|
437
615
|
|
|
438
616
|
For example, if `DATABRICKS_VOLUME_UPLOADS` and `DATABRICKS_VOLUME_EXPORTS` are set, calling `files()` generates two required volume resources validated at startup — no explicit `volumes` config needed.
|
|
439
617
|
|
|
618
|
+
The manifest declares the grant against the **service principal**. For OBO volumes (`auth: "on-behalf-of-user"`), the actual permission requirement is on the **end user** — communicate this out-of-band in your deployment documentation until the manifest schema gains a per-volume auth scope field.
|
|
619
|
+
|
|
440
620
|
## Error responses[](#error-responses "Direct link to Error responses")
|
|
441
621
|
|
|
442
622
|
All errors return JSON:
|