Skip to content

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

Open
ditadi wants to merge 4 commits into
stack/database-stable/02-operation-definitionsfrom
stack/database-stable/03-operation-authorization
Open

ditadi wants to merge 4 commits 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 the answer against its output schema. 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". Without an output schema, undefined answers 204, null on GET, PATCH, or PUT answers 404, and any other value answers 500 without being sent. Spans drop operation input from URLs, and everything below the operation span runs with tracing suppressed.
  • An omitted input schema accepts no parameters and gives authorization/execute undefined. Absent input and an empty object are accepted; non-empty objects and other supplied values are rejected before policies run. No ID, create, update, or replacement input is inferred from the method.
  • Explicit input failures include bounded, schema-declared field paths and standard validation messages. Raw Zod/custom messages, submitted values, record keys, and unknown property names never reach the response. Output failures stay opaque.
  • 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 may omit input, or carry one input query parameter holding JSON in at most 8 KiB of query string. A write may omit its body, or carry one JSON body of at most 1 MiB, and accepts no query string. An explicit input schema decides whether an omitted value is valid.
  • execute receives { actor, input, db, appkit, resource }, never the request, its headers, or the token. A policy's resource lookup receives the same context without resource.
Outcome Status Body
Success 200, or 201 for POST; 204 when the operation has no output or answers nothing the answer, or no body
execute answers null on GET, PATCH, or PUT, and no output accepts null 404 { "error": "…" }
execute returns a value but the operation has no output 500 { "error": "…" }
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": ["input"], "message": "This operation accepts no parameters" }] }
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: All 30 HTTP runtime tests and 5 safe-validation formatter tests pass. They cover omitted inputs for every method, unexpected input rejected before authorization, explicit schema parsing, authentication order, policies, transactions, explicit output projection, replies without an output schema, unknown write outcomes, telemetry redaction, nested field paths, array indices, and redaction of custom issue text and unknown keys. Package typecheck passes. The runtime is exercised through real HTTP with fake workspace and database edges.

Stack

#683 → #687 → #688 → #689 → #690 → #691 → #692

Design: #683. Previous layer: #688. Next layer: #690.

Review feedback: refactor(appkit): answer operations without an output schema with no body replaces the default row projection with the reply rules above.


Stack created with GitHub Stacks CLI • Give Feedback 💬

@ditadi
ditadi added this pull request to stack #693 October 23, 2026 15:01
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size report

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

@databricks/appkit

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

dist raw gzip
JS (runtime) 1.3 MB (-4.4 KB) 459 KB (-468 B)
Type declarations 474 KB (+9.1 KB) 171 KB (+2.9 KB)
Source maps 2.5 MB (+2.0 KB) 861 KB (+2.1 KB)
Other 11 KB 3.7 KB
Total 4.3 MB (+6.7 KB) 1.5 MB (+4.6 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 107 KB (+262 B) 2.5 KB (-1 B) 109 KB (+261 B) external 345 KB (+820 B)
./beta 103 KB (-527 B) 482 B 103 KB (-527 B) external 308 KB (-2.5 KB)
./testing 45 KB (+273 B) 32 KB (+2 B) 77 KB (+275 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 176 B
./beta index.js lazy 114 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 (-212 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

github-actions Bot commented Oct 23, 2026 •

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 41903373616 -R databricks/appkit -n appkit-template-0.90.0-pr.38282b8-stack-database-stable-03-operation-authorization-689 -D appkit-pr-689 \
  && unzip -o "appkit-pr-689/appkit-template-0.90.0-pr.38282b8-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.

ditadi and others added 4 commits November 4, 2026 19:47
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>
Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
…body

The runtime no longer compiles tables or projects a default row. Without
an output schema, undefined answers 204, null on GET, PATCH, and PUT
answers 404, and any other value is a fault, which stays an unknown outcome
after a write. The handler no longer takes the schema.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi force-pushed the stack/database-stable/03-operation-authorization branch from 7cbf040 to 4321d7a Compare November 4, 2026 20:06

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