Skip to content

feat(files): In-App Policy support#197

Open
atilafassina wants to merge 7 commits intomainfrom
files/service-principal-policies
Open

feat(files): In-App Policy support#197
atilafassina wants to merge 7 commits intomainfrom
files/service-principal-policies

Conversation

@atilafassina
Copy link
Copy Markdown
Contributor

Summary

  • Policy system: Introduces 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.
  • Service-principal execution on HTTP routes: HTTP routes now always execute as the service principal instead of requiring OBO. Policies become the only app-level gate that distinguishes between users on HTTP routes — the user identity is extracted from the x-forwarded-user header and passed to the policy.
  • Default to 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:

  1. There was no app-level way to restrict what individual users could do (e.g. allow reads but deny uploads).
  2. Revoking a user's UC WRITE_VOLUME grant 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

File What changed
packages/appkit/src/plugins/files/policy.ts New module — FileAction, FileResource, FilePolicyUser types, PolicyDeniedError, and the policy combinator namespace
packages/appkit/src/plugins/files/plugin.ts Wired policy checks into every route handler and the programmatic VolumeHandle API. Removed isInUserContext gating in favor of SP-first execution + policy enforcement
packages/appkit/src/plugins/files/types.ts Added policy?: FilePolicy to VolumeConfig
packages/appkit/src/index.ts Re-exports policy from the top-level barrel
apps/dev-playground/server/index.ts Updated to use policy.allowAll() on the default volume
docs/docs/plugins/files.md Full documentation: permission model diagram, built-in policies, combinators, custom policies, enforcement semantics

Test scenarios (via dev-playground)

  1. policy.allowAll() volume — all CRUD operations succeed (list, read, upload, delete, mkdir)
  2. policy.publicRead() volume (default) — reads succeed, writes return 403
  3. policy.denyAll() volume — all operations return 403
  4. No x-forwarded-user header in production — returns 401 before policy is evaluated
  5. Async policy — policy returning Promise<boolean> is awaited correctly
  6. Programmatic API with policyappkit.files("vol").asUser(req).list() enforces policy with user identity; appkit.files("vol").list() enforces with { isServicePrincipal: true }
  7. Upload size in resourcecontent-length is passed as resource.size to the policy function
  8. Combinatorspolicy.all() short-circuits on first deny, policy.any() short-circuits on first allow, policy.not() inverts

Test plan

  • Unit tests for policy combinators (policy.test.ts — 136 lines, covers all helpers + async + PolicyDeniedError)
  • Unit tests for plugin policy enforcement (plugin.test.ts — 376 new lines covering routes, SDK API, SP identity, user identity, 401/403 responses)
  • Existing plugin tests updated to work with the new policy-first model
  • Manual verification against a Databricks workspace via pnpm dev with the dev-playground

This pull request was AI-assisted by Isaac.

@atilafassina atilafassina self-assigned this Mar 26, 2026
@atilafassina atilafassina marked this pull request as ready for review March 26, 2026 13:56
Copy link
Copy Markdown
Member

@pkosiec pkosiec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
  • PolicyDeniedError messages 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 _extractUser fallback path not tested
  • _executeOrThrow not directly tested
  • publicReadAndList() not tested

Verdict: Not ready

Fix order:

  1. chore: rework TelemetryManager to use Node SDK #1 (P1, path mismatch) — Refactor so each handler extracts its path first, then passes it explicitly to _enforcePolicy via resourceOverrides.path. This eliminates the dual-extraction pattern and closes the policy bypass.
  2. chore: fix ci runners #3/feat: arrow stream #4 (P1, breaking changes) — Document as breaking in changelog/migration guide. Consider whether publicRead() or allowAll() is the right default for backward compat, or add an explicit opt-in flag.
  3. DCO signoff — All 7 commits lack Signed-off-by. Run git rebase --signoff before merge.
  4. 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.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants