Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
41ab7d7
feat(files): policy-gated SP fallback on HTTP routes (phase 1)
atilafassina Apr 23, 2026
b3068fd
feat(appkit): files plugin policy docs and JSDoc (phase 2)
atilafassina Apr 23, 2026
4920702
chore: update dev fallback and docs
atilafassina Apr 23, 2026
b1c5f5b
feat(files): per-volume auth field + _resolveAuth helper (phase 1)
atilafassina Apr 27, 2026
c8de709
feat(appkit): files OBO identity extraction + policy gate (phase 2)
atilafassina Apr 27, 2026
19e0496
feat(appkit): files OBO read routes via runInUserContext (phase 3)
atilafassina Apr 27, 2026
5e255cb
feat(appkit): files OBO write routes + upload-headers test (phase 4)
atilafassina Apr 27, 2026
169e7af
feat(appkit): files asUser routes SDK calls as the user (phase 5)
atilafassina Apr 27, 2026
e662d10
feat(appkit): files.auth_mode span attribute + manifest scope JSDoc (…
atilafassina Apr 27, 2026
9bad900
docs(appkit): files OBO docs, playground demo, changelog (phase 7)
atilafassina Apr 27, 2026
aab163b
fix(appkit): files OBO review fixes — auth strictness, allocation, ca…
atilafassina Apr 27, 2026
0da42b5
fix(appkit): files invalidate-cache await + integration ephemeral ports
atilafassina Apr 27, 2026
bd212d7
fix(appkit): files plugin OBO review hardening (5 findings)
atilafassina Apr 28, 2026
d065496
fix(appkit): files /read atomic 413 + list-cache key parity for ?path=/
atilafassina May 4, 2026
58c468a
fix(appkit): files Copilot review findings — root invalidation, error…
atilafassina May 5, 2026
bba8780
fix(appkit): rename files plugin _extractObiUser → _extractOboUser
atilafassina May 5, 2026
144c5b2
docs(appkit): refresh stale _enforcePolicy NOTE about SDK identity
atilafassina May 5, 2026
b8d1f57
chore(appkit): drop removed autoStart from files integration tests
atilafassina May 6, 2026
4f2f7dc
test(appkit): align files error-handling tests with hardened response…
atilafassina May 15, 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
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
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