Skip to content

feat(appkit): verify database callers and authorize named operations - #689

Draft
ditadi wants to merge 1 commit into
stack/database-stable/02-operation-definitionsfrom
stack/database-stable/03-operation-authorization
Draft

ditadi wants to merge 1 commit into
stack/database-stable/02-operation-definitionsfrom
stack/database-stable/03-operation-authorization

Conversation

@ditadi

@ditadi ditadi commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What did you change, and why?

Why: An operation must run only for a verified caller that its policy admits, and nothing it touches may reach telemetry.

Change (unregistered runtime):

  • Adds the request pipeline: verify the caller, refuse cross-site requests, route by name, parse the input, run resource and allow once, run execute once, then validate and project the answer. Only a literal true admits, and a throwing lookup or policy denies.
  • In Databricks Apps the caller comes only from x-forwarded-access-token, checked with the workspace Current User Me API on every request. Local development uses the developer's credentials for same-origin loopback requests. The database connection stays the service principal.
  • A write whose answer fails after execute returned answers 500 with outcome: "unknown". Default outputs answer timestamps in RFC 3339. Spans drop operation input from URLs, and everything below the operation span runs with tracing suppressed.
  • Safe to deploy alone: nothing registers the handler, so no request reaches it.

Interface

The HTTP contract this handler implements. The last PR of this stack mounts it at /api/database/operations/<name>.

GET /api/database/operations/notes.list?input=%7B%22board_id%22%3A7%7D
x-forwarded-access-token: <user token, set by Databricks Apps>

HTTP/1.1 200 OK
Cache-Control: no-store
x-appkit-database-actor: <hex SHA-256 of the workspace host, NUL, and the actor id>

[{ "id": 12, "author": "8412…", "body": "Ship it", "created_at": "…", "note_events": [] }]
PATCH /api/database/operations/notes.update
Content-Type: application/json
x-forwarded-access-token: <user token>

{ "id": 12, "body": "Shipped" }
  • A GET carries one input query parameter holding JSON, in at most 8 KiB of query string. A write carries one JSON body of at most 1 MiB and no query string.
  • execute receives { actor, input, db, appkit, resource }, never the request, its headers, or the token.
Outcome Status Body
Success 200, 201 for POST, 204 for DELETE the answer
No verified caller (checked before anything else) 401 { "error": "…" }
The workspace identity check cannot answer 503 { "error": "…" }
A cross-site browser request 403 { "error": "…" }
An unknown name, or another method 404 { "error": "…" }
Bad input 400 { "error": "…", "details": [{ "path": ["changes"], "message": "Expected at least one change" }] }
A body over 1 MiB, or not JSON 413 or 415 { "error": "…" }
authorize returns false or throws 403, or 404 for a resource policy { "error": "…" }
authorize returns { deny } 403 for "forbidden", 404 for "not-found" { "error": "…" }
A resource policy finds nothing, or its lookup throws 404 { "error": "…" }
A database error in execute its category: 409, 422, 503, or 500 { "error": "…" }
A write whose answer fails after execute returned 500 { "error": "Database write may have completed", "outcome": "unknown" }

How do you know it works?

Testing: operation-runtime.test.ts serves the handler over real HTTP with a fake workspace and an in-memory data path. It covers 401 before any input is read, the 403 and 404 rules, one execution per request, transactions, the unknown write outcome, and sentinel tokens, inputs, and rows absent from spans and logs. The operation-actor, service-api, codecs, and routed-path tests cover identity, the service API, timestamps, and path matching. The plugin, database, core, telemetry, utils, context, and shared database suites pass (131 files, 2239 tests), with the CRUD route suites unchanged.


Stack created with GitHub Stacks CLI • Give Feedback 💬

Add the operation request pipeline: verify the caller with the

workspace, refuse cross-site requests, parse the input, run the

policy once and execute once, then validate and project the answer.

The pipeline is not registered yet, so no request reaches it.

Default outputs answer timestamps in RFC 3339, and span redaction

matches the routed path.

Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi added this pull request to stack #693 October 23, 2026 15:01
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 1.2 MB (+5.7 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.3 MB (-4.5 KB) 457 KB (-468 B)
Type declarations 476 KB (+13 KB) 172 KB (+3.9 KB)
Source maps 2.5 MB (+7.2 KB) 859 KB (+3.3 KB)
Other 11 KB 3.7 KB
Total 4.2 MB (+15 KB) 1.5 MB (+6.7 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 106 KB (+256 B) 2.5 KB 109 KB (+256 B) external 344 KB (+820 B)
./beta 102 KB (-516 B) 485 B 103 KB (-516 B) external 306 KB (-2.4 KB)
./testing 45 KB (+272 B) 32 KB 77 KB (+272 B) external 224 KB (+820 B)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 26 KB 0 B 26 KB external 74 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 102 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 82 KB
./beta modular.js initial 6.0 KB
./beta stream-manager.js initial 5.9 KB
./beta service-context.js initial 4.2 KB
./beta databricks.js initial 3.3 KB
./beta client.js initial 647 B
./beta index.js initial 20 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 178 B
./beta index.js lazy 115 B
./testing manifest.js initial 32 KB
./testing index.js initial 10 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 28 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.8 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 26 KB

@databricks/appkit-ui

npm tarball (packed): 395 KB (-2.3 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 427 KB (-1.9 KB) 146 KB (-761 B)
Type declarations 266 KB (-900 B) 98 KB (-211 B)
Source maps 849 KB (-4.4 KB) 284 KB (-1.4 KB)
CSS 16 KB 3.2 KB
Total 1.5 MB (-7.2 KB) 531 KB (-2.4 KB)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 2.1 KB (+15 B) 0 B 2.1 KB (+15 B) 0 B 5.0 KB (+22 B)
./react 433 KB 49 KB 481 KB 1.3 MB 177 KB
./react/beta 4.7 KB (-184 B) 0 B 4.7 KB (-184 B) 0 B 12 KB (-539 B)

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 2.1 KB
./react index.js initial 431 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 4.7 KB

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 39974587375 -R databricks/appkit -n appkit-template-0.89.0-pr.b0cceb0-stack-database-stable-03-operation-authorization-689 -D appkit-pr-689 \
  && unzip -o "appkit-pr-689/appkit-template-0.89.0-pr.b0cceb0-stack-database-stable-03-operation-authorization-689.zip" -d "appkit-pr-689" \
  && databricks apps init --template "appkit-pr-689"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

This branch has not been deployed

No deployments
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.

1 participant