Files
windmill/backend/tests/app_ctx_inputs.rs
Ruben FiszelandClaude Opus 5.5 7627c1be8d fix: fill app ctx inputs from the viewer's session, not the request (#11698)
* fix: fill raw app ctx inputs on the server, not from the request

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: read declared ctx inputs from the deployed app at run time

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: count the revocation's cache invalidation apart from the deploy's

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: drop the deploy-time cache eviction trigger, rely on the cache ttl

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:47:59 +00:00

89 lines
3.3 KiB
Rust

//! A context input a raw app runnable declares (`fields.<arg> = { type: "ctx", .. }`) is
//! filled by the server from the viewer's session. Whatever the request sends under that
//! name is discarded: a runnable filters rows on it, so a viewer who could set it would
//! read someone else's.
//!
//! Users from the `base` fixture:
//! test-user (admin, token SECRET_TOKEN)
//! test-user-2 (developer, token SECRET_TOKEN_2)
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
const ADMIN_TOKEN: &str = "SECRET_TOKEN";
const VIEWER_TOKEN: &str = "SECRET_TOKEN_2";
const APP_PATH: &str = "u/test-user/ctx_app";
const CONTENT: &str = "export function main(who: string) { return who }";
// sha256 of CONTENT, the key an inline runnable is pinned under.
const CONTENT_SHA: &str = "6e198199621d57e4fed834510cb0c2692274aa4cbe976e1704fab2806be9d744";
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
builder.header("Authorization", format!("Bearer {}", token))
}
#[sqlx::test(fixtures("base"))]
async fn a_declared_ctx_input_ignores_the_value_the_viewer_sends(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let ws = format!("http://localhost:{port}/api/w/test-workspace");
let client = reqwest::Client::new();
let resp = authed(client.post(format!("{ws}/apps/create")), ADMIN_TOKEN)
.json(&json!({
"path": APP_PATH,
"summary": "ctx inputs",
"value": {
"runnables": {
"whoami": {
"type": "inline",
"inlineScript": { "language": "bun", "content": CONTENT },
"fields": { "who": { "type": "ctx", "ctx": "username" } }
}
}
},
// The policy says nothing of context inputs: the server reads the
// declaration from the deployed runnable.
"policy": {
"execution_mode": "anonymous",
"triggerables_v2": {
format!("whoami:rawscript/{CONTENT_SHA}"): {
"static_inputs": {},
"one_of_inputs": {}
}
}
}
}))
.send()
.await?;
assert!(resp.status().is_success(), "{}", resp.text().await?);
// Both spellings a viewer could send: a forged literal, and no argument at all.
for args in [json!({ "who": "test-user" }), json!({})] {
let resp = authed(
client.post(format!("{ws}/apps_u/execute_component/{APP_PATH}")),
VIEWER_TOKEN,
)
.json(&json!({
"component": "whoami",
"args": args,
"raw_code": { "language": "bun", "content": CONTENT }
}))
.send()
.await?;
assert!(resp.status().is_success(), "{}", resp.text().await?);
let job_id: uuid::Uuid = resp.text().await?.parse()?;
let queued: serde_json::Value = sqlx::query_scalar("SELECT args FROM v2_job WHERE id = $1")
.bind(job_id)
.fetch_one(&db)
.await?;
assert_eq!(queued["who"], json!("test-user-2"), "sent {args}");
}
Ok(())
}