diff --git a/backend/.sqlx/query-0476ae2245aa678a50c5fd04cdee32cc151e29b177cc85caa088430b16336373.json b/backend/.sqlx/query-0476ae2245aa678a50c5fd04cdee32cc151e29b177cc85caa088430b16336373.json new file mode 100644 index 0000000000..5ae11837c7 --- /dev/null +++ b/backend/.sqlx/query-0476ae2245aa678a50c5fd04cdee32cc151e29b177cc85caa088430b16336373.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT flow_version.path FROM flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE flow_version.id = $1 AND flow_version.workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0476ae2245aa678a50c5fd04cdee32cc151e29b177cc85caa088430b16336373" +} diff --git a/backend/.sqlx/query-e70cbc2a48bfc5c7d2018b9367eadc104e87126c57230c9ed8eb3987e54b53a1.json b/backend/.sqlx/query-e70cbc2a48bfc5c7d2018b9367eadc104e87126c57230c9ed8eb3987e54b53a1.json new file mode 100644 index 0000000000..48981ce580 --- /dev/null +++ b/backend/.sqlx/query-e70cbc2a48bfc5c7d2018b9367eadc104e87126c57230c9ed8eb3987e54b53a1.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM flow_version WHERE id = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "e70cbc2a48bfc5c7d2018b9367eadc104e87126c57230c9ed8eb3987e54b53a1" +} diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index ff72ddce70..dcec542cb8 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -112,9 +112,9 @@ use windmill_common::{ }; use windmill_common::{ - get_flow_version_info_from_version, get_latest_deployed_hash_for_path, - get_latest_flow_version_info_for_path, get_script_info_for_hash, utils::empty_as_none, - ScriptHashInfo, BASE_URL, + get_flow_path_for_version_authed, get_flow_version_info_from_version, + get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path, + get_script_info_for_hash, utils::empty_as_none, ScriptHashInfo, BASE_URL, }; use windmill_queue::{ get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs, PushArgsOwned, @@ -4048,21 +4048,8 @@ pub async fn run_flow_by_version_inner( #[cfg(feature = "enterprise")] check_license_key_valid().await?; - let flow_path = sqlx::query_scalar!( - r#" - SELECT - path - FROM - flow_version - WHERE - id = $1 AND - workspace_id = $2 - "#, - version, - &w_id - ) - .fetch_one(&db) - .await?; + let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; + let flow_path = get_flow_path_for_version_authed(&userdb_authed, &db, version, &w_id).await?; check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; @@ -5550,13 +5537,8 @@ pub async fn run_wait_result_flow_by_version_get( #[cfg(feature = "enterprise")] check_license_key_valid().await?; - let flow_path = sqlx::query_scalar!( - "SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2", - version, - &w_id - ) - .fetch_one(&db) - .await?; + let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; + let flow_path = get_flow_path_for_version_authed(&userdb_authed, &db, version, &w_id).await?; check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; @@ -5606,21 +5588,8 @@ pub async fn run_wait_result_flow_by_version( #[cfg(feature = "enterprise")] check_license_key_valid().await?; - let flow_path = sqlx::query_scalar!( - r#" - SELECT - path - FROM - flow_version - WHERE - id = $1 AND - workspace_id = $2 - "#, - version, - &w_id - ) - .fetch_one(&db) - .await?; + let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; + let flow_path = get_flow_path_for_version_authed(&userdb_authed, &db, version, &w_id).await?; check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 39884e3d35..637b32860b 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -1543,6 +1543,59 @@ pub fn get_flow_version_info_from_version< } } +/// Resolve a `flow_version.id` to its flow path while enforcing the caller's +/// folder-level ACL. The `flow_version` table has no row-level security, so the +/// authorization gate is an RLS-filtered lookup against the `flow` table through +/// `user_db`. Mirrors the "exists but not authorized -> NotAuthorized" semantics +/// of [`get_latest_flow_version_id_for_path`] so version-keyed run routes are +/// gated identically to their path-keyed siblings. +pub async fn get_flow_path_for_version_authed( + db_authed: &UserDbWithAuthed<'_, AuthedRef<'_>>, + db: &DB, + version: i64, + w_id: &str, +) -> error::Result { + let mut conn = db_authed.acquire().await?; + let authed_path = sqlx::query_scalar!( + "SELECT flow_version.path FROM flow_version + INNER JOIN flow + ON flow.path = flow_version.path AND + flow.workspace_id = flow_version.workspace_id + WHERE flow_version.id = $1 AND flow_version.workspace_id = $2", + version, + w_id, + ) + .fetch_optional(&mut *conn) + .await?; + + if let Some(path) = authed_path { + return Ok(path); + } + + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM flow_version WHERE id = $1 AND workspace_id = $2)", + version, + w_id, + ) + .fetch_one(db) + .await? + .unwrap_or(false); + + if exists { + // Unlike the path-keyed sibling (where the caller already supplied the + // path), here the caller only supplied an opaque version id. Echoing + // back the resolved path would disclose an id->path mapping for a flow + // they cannot access, so the message is intentionally generic. + return Err(Error::NotAuthorized( + "You are not authorized to run this flow version".to_string(), + )); + } + + Err(Error::NotFound(format!( + "flow_version not found at id {version}" + ))) +} + pub async fn get_latest_flow_version_info_for_path<'e>( db_authed: Option>>, db: &DB,