From f2f7ce8e964ce31f1815b8db3c0862cd07bc7bcb Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 12 Sep 2026 11:56:33 +0200 Subject: [PATCH] test: pin that an app draft read resolves no address Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EYWxvwmteVyzcWenJfbnzs --- backend/tests/preserve_on_behalf_of.rs | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/backend/tests/preserve_on_behalf_of.rs b/backend/tests/preserve_on_behalf_of.rs index 75f42fe58c..1847260427 100644 --- a/backend/tests/preserve_on_behalf_of.rs +++ b/backend/tests/preserve_on_behalf_of.rs @@ -2885,6 +2885,70 @@ async fn test_schedule_permissions_superadmin_not_in_workspace( Ok(()) } +/// An app read derives the address from the principal the *server* stored, and only from that +/// one. A draft's principal is whatever its editor last typed, so deriving one would answer +/// "which address is `u/x`?" for any principal a caller names — `superadmin-external` is a +/// superadmin of no workspace, whose address `resolve_username_to_email` reaches instance-wide. +#[sqlx::test(fixtures("preserve_on_behalf_of"))] +async fn test_app_draft_read_does_not_derive_an_address(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let base = format!( + "http://localhost:{}/api/w/test-workspace", + server.addr.port() + ); + + let resp = authed(client().post(format!("{base}/apps/create")), "SECRET_TOKEN") + .json(&new_app_with_on_behalf_of( + "u/test-user/app_with_draft", + Some("u/original-user"), + None, + true, + )) + .send() + .await?; + assert_eq!(resp.status(), 201, "{}", resp.text().await?); + + sqlx::query( + r#"INSERT INTO draft (workspace_id, path, typ, value, email) + VALUES ('test-workspace', 'u/test-user/app_with_draft', 'app', $1::text::json, 'test@windmill.dev')"#, + ) + .bind(json!({ "policy": { "on_behalf_of": "u/superadmin-external" } }).to_string()) + .execute(&db) + .await?; + + let resp = authed( + client().get(format!( + "{base}/apps/get/p/u/test-user/app_with_draft?get_draft=true" + )), + "SECRET_TOKEN", + ) + .send() + .await?; + let body: serde_json::Value = resp.json().await?; + + assert_eq!( + body["policy"]["on_behalf_of_email"].as_str(), + Some("original@windmill.dev"), + "the deployed policy carries the address derived from its principal" + ); + // Asserted before the absence below, so that a response carrying no draft at all cannot + // pass this test by having nothing to derive from. + assert_eq!( + body["draft"]["policy"]["on_behalf_of"].as_str(), + Some("u/superadmin-external"), + "the draft is returned, principal and all: {body}" + ); + assert!( + body["draft"]["policy"]["on_behalf_of_email"].is_null(), + "the draft's principal must not be resolved: {}", + body["draft"] + ); + + Ok(()) +} + // ============================================================================ // Forged-superadmin on_behalf_of guard (GHSA-hfh4-cx4h-3fcr) // ============================================================================