Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f95ad08
feat(files): policy-gated SP fallback on HTTP routes (phase 1)
atilafassina Apr 23, 2026
09bce40
feat(appkit): files plugin policy docs and JSDoc (phase 2)
atilafassina Apr 23, 2026
1462a39
chore: update dev fallback and docs
atilafassina Apr 23, 2026
69db9a0
feat(files): per-volume auth field + _resolveAuth helper (phase 1)
atilafassina Apr 27, 2026
cb0609b
feat(appkit): files OBO identity extraction + policy gate (phase 2)
atilafassina Apr 27, 2026
a951213
feat(appkit): files OBO read routes via runInUserContext (phase 3)
atilafassina Apr 27, 2026
188c2d3
feat(appkit): files OBO write routes + upload-headers test (phase 4)
atilafassina Apr 27, 2026
c6533de
feat(appkit): files asUser routes SDK calls as the user (phase 5)
atilafassina Apr 27, 2026
6747d4a
feat(appkit): files.auth_mode span attribute + manifest scope JSDoc (…
atilafassina Apr 27, 2026
ad94a7f
docs(appkit): files OBO docs, playground demo, changelog (phase 7)
atilafassina Apr 27, 2026
f7d8338
fix(appkit): files OBO review fixes — auth strictness, allocation, ca…
atilafassina Apr 27, 2026
5e26514
fix(appkit): files invalidate-cache await + integration ephemeral ports
atilafassina Apr 27, 2026
8d5bdca
fix(appkit): files plugin OBO review hardening (5 findings)
atilafassina Apr 28, 2026
3dac8fd
fix(appkit): files /read atomic 413 + list-cache key parity for ?path=/
atilafassina May 4, 2026
14197a0
fix(appkit): files Copilot review findings — root invalidation, error…
atilafassina May 5, 2026
6ee9560
fix(appkit): rename files plugin _extractObiUser → _extractOboUser
atilafassina May 5, 2026
e6fd266
docs(appkit): refresh stale _enforcePolicy NOTE about SDK identity
atilafassina May 5, 2026
c453606
chore(appkit): drop removed autoStart from files integration tests
atilafassina May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/dev-playground/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ env:
valueFrom: volume
- name: DATABRICKS_VOLUME_IMPLICIT
valueFrom: volume
# OBO demo: same physical volume; auth: "on-behalf-of-user" routes
# HTTP traffic through runInUserContext so SDK calls execute as the
# end user.
- name: DATABRICKS_VOLUME_OBO_DEMO
valueFrom: volume
73 changes: 73 additions & 0 deletions apps/dev-playground/client/src/routes/policy-matrix.route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ function PolicyMatrixRoute() {
const [runningAll, setRunningAll] = useState(false);
const [spResult, setSpResult] = useState<string | null>(null);
const [oboResult, setOboResult] = useState<string | null>(null);
const [oboVolumeResult, setOboVolumeResult] = useState<string | null>(null);
const [oboVolumeHttpResult, setOboVolumeHttpResult] = useState<string | null>(
null,
);

useEffect(() => {
fetch("/whoami")
Expand Down Expand Up @@ -197,6 +201,40 @@ function PolicyMatrixRoute() {
setOboResult(JSON.stringify(await r.json(), null, 2));
}, []);

/**
* Programmatic OBO-volume smoke. Calls the dev-playground's
* `/policy/obo-volume` route which hits `appkit.files("obo_demo")` —
* a volume configured with `auth: "on-behalf-of-user"` — through both
* `asUser(req)` and the bare callable. The browser automatically
* forwards `x-forwarded-user` / `x-forwarded-access-token` when running
* behind the Databricks Apps reverse proxy; locally they're absent and
* the dev fallback reports `service-principal` execution.
*/
const runOboVolumeSmoke = useCallback(async () => {
setOboVolumeResult("…");
const r = await fetch("/policy/obo-volume");
setOboVolumeResult(JSON.stringify(await r.json(), null, 2));
}, []);

/**
* Direct HTTP probe against the OBO volume's `/list` route. Confirms
* end-to-end that the route handler routes the SDK call through
* `runInUserContext` when the headers are present, and returns 401 (or
* 403, in dev fallback) when they're missing.
*/
const runOboVolumeHttp = useCallback(async () => {
setOboVolumeHttpResult("…");
try {
const r = await fetch(`/api/files/obo_demo/list`);
const body = await r.json().catch(() => ({}) as Record<string, unknown>);
setOboVolumeHttpResult(
JSON.stringify({ httpStatus: r.status, body }, null, 2),
);
} catch (err) {
setOboVolumeHttpResult(err instanceof Error ? err.message : String(err));
}
}, []);

const reset = useCallback(() => setState(initialState), [initialState]);

return (
Expand Down Expand Up @@ -297,6 +335,41 @@ function PolicyMatrixRoute() {
<SmokePanel title="On-behalf-of user" body={oboResult} />
</div>
</div>

<div className="mt-10">
<h2 className="text-xl font-semibold mb-2">
Per-volume OBO mode (<code>auth: "on-behalf-of-user"</code>)
</h2>
<p className="text-sm text-muted-foreground mb-4">
Hits the <code>obo_demo</code> volume — configured with{" "}
<code>auth: "on-behalf-of-user"</code> — to confirm SDK calls
execute as the end user when the request carries{" "}
<code>x-forwarded-access-token</code> +{" "}
<code>x-forwarded-user</code>. In the deployed Databricks App those
headers are injected by the platform reverse proxy. Locally they're
absent and the dev-mode fallback applies: <em>HTTP returns 403</em>{" "}
(the <code>usersOnly</code> policy denies SP traffic) and the
programmatic path runs as the SP.
</p>
<div className="flex gap-3 mb-4">
<Button variant="outline" onClick={runOboVolumeHttp}>
Hit /api/files/obo_demo/list
</Button>
<Button variant="outline" onClick={runOboVolumeSmoke}>
Run OBO-volume programmatic smoke
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<SmokePanel
title="HTTP — /api/files/obo_demo/list"
body={oboVolumeHttpResult}
/>
<SmokePanel
title="Programmatic — appkit.files('obo_demo').asUser(req).list()"
body={oboVolumeResult}
/>
</div>
</div>
</div>
</div>
);
Expand Down
54 changes: 54 additions & 0 deletions apps/dev-playground/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ const adminOnly: FilePolicy = (action, _resource, user) => {
return true;
};

/**
* OBO demo policy: deny anything running as the SP (including the dev
* fallback when no `x-forwarded-access-token` is present). Only real
* end-users (`isServicePrincipal: false`) get through.
*/
const usersOnly: FilePolicy = (_action, _resource, user) => {
return user.isServicePrincipal !== true;
};

createApp({
plugins: [
server(),
Expand Down Expand Up @@ -80,6 +89,14 @@ createApp({
write_only: { policy: files.policy.not(files.policy.publicRead()) },
// no explicit policy → falls back to publicRead() + startup warning
implicit: {},
// OBO demo volume — auth: "on-behalf-of-user" routes HTTP traffic
// through `runInUserContext` so SDK calls execute with the end
// user's access token. The `usersOnly` policy denies any traffic
// that wasn't authenticated via `x-forwarded-access-token`.
obo_demo: {
auth: "on-behalf-of-user",
policy: usersOnly,
},
},
}),
jobs(),
Expand Down Expand Up @@ -196,6 +213,43 @@ createApp({
results,
});
});

/**
* Per-volume OBO mode demo. Hits the `obo_demo` volume — configured
* with `auth: "on-behalf-of-user"` — to confirm:
*
* 1. With a forwarded user identity, HTTP routes execute the SDK
* call as the end user (request goes through `runInUserContext`).
* 2. Without `x-forwarded-access-token`, production returns 401;
* development falls back to the SP and the `usersOnly` policy
* rejects with 403.
* 3. Programmatic `appkit.files("obo_demo").asUser(req).list()` runs
* inside the same user context.
*
* Returns the HTTP status, body, and the user identity the server
* observes — so the policy-matrix client can render a clear
* pass/fail panel.
*/
app.get("/policy/obo-volume", async (req, res) => {
const xForwardedUser = req.header("x-forwarded-user") ?? null;
const xForwardedToken =
(req.header("x-forwarded-access-token")?.length ?? 0) > 0;

const programmatic: ProbeResult[] = await runProbes([
[
"obo_demo",
"list",
() => appkit.files("obo_demo").asUser(req).list(),
],
]);

res.json({
mode: "on-behalf-of-user",
xForwardedUser,
xForwardedAccessTokenPresent: xForwardedToken,
programmatic,
});
});
});
},
}).catch(console.error);
Expand Down
28 changes: 27 additions & 1 deletion docs/docs/api/appkit/Interface.FilePolicyUser.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ Minimal user identity passed to the policy function.
id: string;
```

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.

***

### isServicePrincipal?
Expand All @@ -18,4 +23,25 @@ id: string;
optional isServicePrincipal: boolean;
```

`true` when the caller is the service principal (direct SDK call, not `asUser`).
`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.

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:

| Volume `auth` | Path | Headers | `isServicePrincipal` | Notes |
| --------------------- | ------------------------------ | ----------------------------- | -------------------- | ---------------------------------------------------------------------------------------------- |
| `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. |
| `service-principal` | HTTP | no `x-forwarded-user` | `true` | Headerless request — policy and SDK both run as the SP. |
| `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. |
| `on-behalf-of-user` | HTTP | missing token, dev-fallback | `true` | Only reachable when `NODE_ENV === "development"` (prod returns 401). Treated as SP traffic. |
| any | Programmatic `asUser(req)` | `x-forwarded-user` present | `false` | `asUser` extracts the user; the SDK call runs as the user inside `runInUserContext`. |

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)`.
Loading
Loading