Files
windmill/backend/tests/capture_cross_workspace.rs
hugocasaandClaude Opus 5 40f0cab2ad fix: scope capture deletion to the workspace in the request path (#10795)
* fix: scope capture deletion to the workspace in the request path

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

* test: layer the capture fixture on base instead of duplicating it

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:01:25 +00:00

56 lines
2.0 KiB
Rust

//! `DELETE /w/{workspace}/capture/{id}` must stay inside the workspace in the URL.
//!
//! Capture ids come from one instance-wide sequence and the capture RLS policies
//! key on the path segment only, never on `workspace_id` — so an id alone is not
//! an authorization boundary. A member of one workspace can name any id and, if
//! the row's path happens to sit inside their grants (`u/<their username>/…`,
//! `g/<their group>/…`, a same-named folder), reach a row belonging to a
//! workspace they are not a member of.
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
async fn capture_workspace(db: &Pool<Postgres>, id: i64) -> Option<String> {
sqlx::query_scalar::<_, String>("SELECT workspace_id FROM capture WHERE id = $1")
.bind(id)
.fetch_optional(db)
.await
.unwrap()
}
#[sqlx::test(fixtures("base", "capture_cross_workspace"))]
async fn delete_capture_is_confined_to_the_url_workspace(db: Pool<Postgres>) -> 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 client = reqwest::Client::new();
// Capture 1 lives in test-workspace-2, which this member has no access to.
let resp = client
.delete(format!("{base}/capture/1"))
.header("Authorization", "Bearer SECRET_TOKEN_2")
.send()
.await?;
let status = resp.status();
assert_eq!(
capture_workspace(&db, 1).await.as_deref(),
Some("test-workspace-2"),
"capture of another workspace was deleted (status {status})"
);
// The member's own capture in test-workspace still deletes.
let resp = client
.delete(format!("{base}/capture/2"))
.header("Authorization", "Bearer SECRET_TOKEN_2")
.send()
.await?;
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
assert_eq!(capture_workspace(&db, 2).await, None);
Ok(())
}