mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
* 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>
89 lines
3.3 KiB
Rust
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(())
|
|
}
|