Conversation
pkosiec
left a comment
There was a problem hiding this comment.
Great work! Overall LGTM, pls see my small comments on the proposal and here - they might affect some methods naming etc.
Disclaimer: unfortunately I wasn't able to test it, I'll run it next week 👍 Unless you could record a short, raw demo video showcasing how the policies work ?
Also, I think it would be worth if we had approval for at least the RFC from Fabian / Mario who were much more involved in the files discussions. Thanks!
There was a problem hiding this comment.
Not sure if we shouldn't update the AppKit template to utilize the policies for the sample path?
And the same with an agent skill for files? Agent might be confused without additional guidance - we need to verify that.
There was a problem hiding this comment.
Please verify and address the agentic review comments:
Code Review: feat(files): In-App Policy support
Scope: 17 files changed, +1318/-224 lines against main
Intent: Replace OBO-enforcement authorization with SP-first execution + composable policy functions. Policies become the only app-level user gate on HTTP routes. Default: publicRead().
Review team:
- correctness (always)
- testing (always)
- maintainability (always)
- project-standards (always)
- security -- new policy/auth layer, x-forwarded-user header trust
- api-contract -- removed export, new public API, execution model change
- adversarial -- 1300+ lines touching auth/authorization
- kieran-typescript -- TypeScript plugin code, types, policy module
P1 -- High
| # | File | Issue | Reviewer(s) | Confidence | Route |
|---|---|---|---|---|---|
| 1 | plugin.ts:186 |
Policy path mismatch enables bypass: _enforcePolicy extracts path via query.path ?? body.path ?? "/" but handlers extract independently (mkdir uses body.path, upload/delete use query.path). An attacker can send conflicting paths in query vs body to have policy check one path while the operation acts on another. E.g. POST /mkdir?path=public/ok with body: {path: "admin/secret"} — policy checks public/ok, mkdir creates admin/secret. |
correctness, security, adversarial | 0.92 | manual → human |
| 2 | plugin.ts:111 |
x-forwarded-user trusted without proxy validation: _extractUser reads header directly. If the app is reachable without a trusted reverse proxy that strips/re-adds this header, any client can impersonate any user. All per-user policy enforcement depends on this header's integrity. |
security, adversarial | 0.88 | advisory → human |
| 3 | plugin.ts:225 + types.ts |
Breaking: default publicRead() silently denies writes for upgrading users: Volumes without explicit policy get publicRead(), which denies upload/mkdir/delete. Previously these worked via OBO. No migration guide or opt-in flag. |
api-contract, adversarial | 0.88 | advisory → release |
| 4 | All route handlers | Breaking: HTTP routes now execute as SP instead of OBO: Changes execution principal for all file operations. Users relying on per-user UC grants lose that enforcement layer. isInUserContext() removed (breaking import). |
api-contract | 0.90 | advisory → release |
P2 -- Moderate
| # | File | Issue | Reviewer(s) | Confidence | Route |
|---|---|---|---|---|---|
| 5 | plugin.test.ts |
Missing policy enforcement tests for 7/10 HTTP handlers: Only list, mkdir, upload have dedicated policy tests. read, download, raw, exists, metadata, preview, delete all call _enforcePolicy but lack tests. |
testing | 0.95 | manual → downstream-resolver |
| 6 | policy.test.ts |
Empty combinator arrays untested: policy.all() returns true (vacuous truth — allows everything), policy.any() returns false. Neither behavior is tested or documented. all() with empty args is a footgun. |
adversarial, testing | 0.80 | manual → downstream-resolver |
| 7 | plugin.ts:166,254 |
Path extraction duplication: _enforcePolicy and each handler independently extract path from req. Two sources of truth for the same value. Root cause of finding #1. |
maintainability, correctness | 0.85 | manual → human |
| 8 | plugin.ts:254,1017 |
Code duplication: createVolumeAPI and _createPolicyWrappedAPI: Both define the same 9 methods with identical structure. Only difference is policy check wrapper. Adding a new method requires updating both. |
maintainability | 0.88 | manual → downstream-resolver |
| 9 | plugin.test.ts |
SDK asUser() policy tested for list() only: 8 other VolumeAPI methods lack asUser() policy enforcement tests. |
testing | 0.75 | manual → downstream-resolver |
| 10 | plugin.ts:144 |
No timeout on async policy evaluation: Custom policies returning Promise<boolean> can hang indefinitely, blocking the HTTP request with no per-policy timeout. |
adversarial | 0.70 | advisory → human |
P3 -- Low
| # | File | Issue | Reviewer(s) | Confidence | Route |
|---|---|---|---|---|---|
| 11 | plugin.ts:102 |
Dead code: _hasPolicy() always returns true since constructor assigns default policy to every volume |
maintainability | 0.95 | safe_auto → review-fixer |
| 12 | plugin.ts:110 |
Inconsistent Express type imports: Top-level import type express at line 3, but import("express").Request used inline in private methods |
kieran-typescript | 0.85 | safe_auto → review-fixer |
| 13 | policy.ts:139 |
publicReadAndList() is an untested alias for publicRead() with unclear justification |
maintainability, testing | 0.78 | advisory → human |
| 14 | plugin.ts:1131 |
Lazy SP user getter: get id() { return getCurrentUserId() } — semantically mismatches id: string interface, try-catch around logging suggests it can fail |
kieran-typescript, security | 0.68 | advisory → human |
| 15 | plugin.ts:146 |
Error swallowing in policy denial logging: catch block silently swallows all errors from logger.warn(user.id) |
adversarial | 0.60 | advisory → human |
Pre-existing issues
None identified.
Coverage
- Suppressed: 0 findings below 0.60 confidence
- Untracked files: None
- Failed reviewers: 0 of 8
- Intent confidence: High (PR body is detailed)
Residual risks
- Cache keys now use SP user ID instead of per-user. If OBO caching behavior was relied upon, data could leak across users sharing the SP cache.
PolicyDeniedErrormessages reveal volume names and action to clients (minor info leak).- TOCTOU between policy check and file operation is inherent to this design.
Testing gaps
- 7 of 10 HTTP route handlers lack dedicated policy enforcement tests
- Empty combinator arrays (
all(),any()) untested - Path-based custom policies not tested (policy checking
resource.path) - Dev-mode
_extractUserfallback path not tested _executeOrThrownot directly testedpublicReadAndList()not tested
Verdict: Not ready
Fix order:
- chore: rework TelemetryManager to use Node SDK #1 (P1, path mismatch) — Refactor so each handler extracts its path first, then passes it explicitly to
_enforcePolicyviaresourceOverrides.path. This eliminates the dual-extraction pattern and closes the policy bypass. - chore: fix ci runners #3/feat: arrow stream #4 (P1, breaking changes) — Document as breaking in changelog/migration guide. Consider whether
publicRead()orallowAll()is the right default for backward compat, or add an explicit opt-in flag. - DCO signoff — All 7 commits lack
Signed-off-by. Rungit rebase --signoffbefore merge. - chore: tune tsdown configuration to reduce bundle size #5 (P2, test coverage) — Add policy enforcement tests for the remaining handlers before merge.
Findings #2, #6-#10 should be addressed but are not merge-blocking if #1 is fixed.
Summary
FilePolicy, a per-volume function(action, resource, user) → boolean | Promise<boolean>that gates every file operation before the Databricks API call is made. Ships with built-in helpers (policy.publicRead(),policy.allowAll(),policy.denyAll()) and combinators (all,any,not) for composition.x-forwarded-userheader and passed to the policy.publicRead(): Volumes without an explicit policy default to read-only access. A startup warning encourages setting an explicit policy.Motivation
Previously the files plugin relied entirely on OBO (on-behalf-of) token forwarding and Unity Catalog grants to restrict access. This meant:
WRITE_VOLUMEgrant had no effect on HTTP routes since the SP's credentials were used anyway.Policies close this gap by giving developers a composable, per-volume authorization layer evaluated before any API call.
Key changes
packages/appkit/src/plugins/files/policy.tsFileAction,FileResource,FilePolicyUsertypes,PolicyDeniedError, and thepolicycombinator namespacepackages/appkit/src/plugins/files/plugin.tsVolumeHandleAPI. RemovedisInUserContextgating in favor of SP-first execution + policy enforcementpackages/appkit/src/plugins/files/types.tspolicy?: FilePolicytoVolumeConfigpackages/appkit/src/index.tspolicyfrom the top-level barrelapps/dev-playground/server/index.tspolicy.allowAll()on the default volumedocs/docs/plugins/files.mdTest scenarios (via dev-playground)
policy.allowAll()volume — all CRUD operations succeed (list, read, upload, delete, mkdir)policy.publicRead()volume (default) — reads succeed, writes return403policy.denyAll()volume — all operations return403x-forwarded-userheader in production — returns401before policy is evaluatedPromise<boolean>is awaited correctlyappkit.files("vol").asUser(req).list()enforces policy with user identity;appkit.files("vol").list()enforces with{ isServicePrincipal: true }content-lengthis passed asresource.sizeto the policy functionpolicy.all()short-circuits on first deny,policy.any()short-circuits on first allow,policy.not()invertsTest plan
policy.test.ts— 136 lines, covers all helpers + async +PolicyDeniedError)plugin.test.ts— 376 new lines covering routes, SDK API, SP identity, user identity, 401/403 responses)pnpm devwith the dev-playgroundThis pull request was AI-assisted by Isaac.