diff --git a/backend/.sqlx/query-1debd472c9ffd2fc78877484f93db51f9aabed54f9894eda8ad610053ad76ce6.json b/backend/.sqlx/query-83e9c830ee816d4e4e7193a67823cab7c9a4461d3bfbdc0fe7dbdd28ae2de924.json similarity index 56% rename from backend/.sqlx/query-1debd472c9ffd2fc78877484f93db51f9aabed54f9894eda8ad610053ad76ce6.json rename to backend/.sqlx/query-83e9c830ee816d4e4e7193a67823cab7c9a4461d3bfbdc0fe7dbdd28ae2de924.json index 96147d531e..b6a5d2a2d0 100644 --- a/backend/.sqlx/query-1debd472c9ffd2fc78877484f93db51f9aabed54f9894eda8ad610053ad76ce6.json +++ b/backend/.sqlx/query-83e9c830ee816d4e4e7193a67823cab7c9a4461d3bfbdc0fe7dbdd28ae2de924.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT restart_unless_cancelled, timeout FROM script WHERE hash = $1 AND workspace_id = $2", + "query": "SELECT restart_unless_cancelled, timeout, job_token_scopes FROM script WHERE hash = $1 AND workspace_id = $2", "describe": { "columns": [ { @@ -12,6 +12,11 @@ "ordinal": 1, "name": "timeout", "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "job_token_scopes", + "type_info": "TextArray" } ], "parameters": { @@ -21,9 +26,10 @@ ] }, "nullable": [ + true, true, true ] }, - "hash": "1debd472c9ffd2fc78877484f93db51f9aabed54f9894eda8ad610053ad76ce6" + "hash": "83e9c830ee816d4e4e7193a67823cab7c9a4461d3bfbdc0fe7dbdd28ae2de924" } diff --git a/backend/.sqlx/query-df8586283178b0e684b9a0e359efb16e38fd68a5fe2b01cd77f74a725bbf37a3.json b/backend/.sqlx/query-aed606f8c8d91a924ac457b4eabdf391947d5fc3da90ef98a7b793c2430eb7bc.json similarity index 76% rename from backend/.sqlx/query-df8586283178b0e684b9a0e359efb16e38fd68a5fe2b01cd77f74a725bbf37a3.json rename to backend/.sqlx/query-aed606f8c8d91a924ac457b4eabdf391947d5fc3da90ef98a7b793c2430eb7bc.json index 3383951c13..0bd5d0eca5 100644 --- a/backend/.sqlx/query-df8586283178b0e684b9a0e359efb16e38fd68a5fe2b01cd77f74a725bbf37a3.json +++ b/backend/.sqlx/query-aed606f8c8d91a924ac457b4eabdf391947d5fc3da90ef98a7b793c2430eb7bc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT q.id AS \"id!\", j.created_by, j.permissioned_as, j.permissioned_as_email, j.trigger, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.preprocessed, j.args AS \"args: sqlx::types::Json>>\" FROM v2_job_queue q JOIN v2_job j USING (id) JOIN script s ON s.workspace_id = j.workspace_id AND s.hash = j.runnable_id WHERE j.workspace_id = $1 AND j.runnable_path = $2 AND j.kind = 'script' AND j.flow_step_id IS NULL AND j.runnable_id != $3 AND q.canceled_by IS NULL AND s.restart_unless_cancelled", + "query": "SELECT q.id AS \"id!\", j.created_by, j.permissioned_as, j.permissioned_as_email, j.trigger, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.preprocessed, j.args AS \"args: sqlx::types::Json>>\", p.job_token_scopes AS \"job_token_scopes?\" FROM v2_job_queue q JOIN v2_job j USING (id) JOIN script s ON s.workspace_id = j.workspace_id AND s.hash = j.runnable_id LEFT JOIN job_perms p ON p.job_id = q.id WHERE j.workspace_id = $1 AND j.runnable_path = $2 AND j.kind = 'script' AND j.flow_step_id IS NULL AND j.runnable_id != $3 AND q.canceled_by IS NULL AND s.restart_unless_cancelled", "describe": { "columns": [ { @@ -71,6 +71,11 @@ "ordinal": 7, "name": "args: sqlx::types::Json>>", "type_info": "Jsonb" + }, + { + "ordinal": 8, + "name": "job_token_scopes?", + "type_info": "TextArray" } ], "parameters": { @@ -88,8 +93,9 @@ true, true, true, + true, true ] }, - "hash": "df8586283178b0e684b9a0e359efb16e38fd68a5fe2b01cd77f74a725bbf37a3" + "hash": "aed606f8c8d91a924ac457b4eabdf391947d5fc3da90ef98a7b793c2430eb7bc" } diff --git a/backend/windmill-api-auth/src/lib.rs b/backend/windmill-api-auth/src/lib.rs index 49797d3f9e..30c3bb65af 100644 --- a/backend/windmill-api-auth/src/lib.rs +++ b/backend/windmill-api-auth/src/lib.rs @@ -519,6 +519,11 @@ pub async fn caller_scope_ceiling( let Some(job_id) = authed.job_id else { return Ok(None); }; + // A job token is minted with scopes exactly when its job is restricted, so an unscoped one + // has nothing to read. + if authed.scopes.is_none() { + return Ok(None); + } let row = sqlx::query_scalar!( "SELECT job_token_scopes FROM job_perms WHERE job_id = $1", job_id diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 3e855645b6..e48ff19713 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -727,9 +727,11 @@ async fn restart_perpetual_runs_at_path( PerpetualRunToRestart, "SELECT q.id AS \"id!\", j.created_by, j.permissioned_as, j.permissioned_as_email, \ j.trigger, j.trigger_kind AS \"trigger_kind: TriggerKindLabel\", j.preprocessed, \ - j.args AS \"args: sqlx::types::Json>>\" \ + j.args AS \"args: sqlx::types::Json>>\", \ + p.job_token_scopes AS \"job_token_scopes?\" \ FROM v2_job_queue q JOIN v2_job j USING (id) \ JOIN script s ON s.workspace_id = j.workspace_id AND s.hash = j.runnable_id \ + LEFT JOIN job_perms p ON p.job_id = q.id \ WHERE j.workspace_id = $1 AND j.runnable_path = $2 AND j.kind = 'script' \ AND j.flow_step_id IS NULL AND j.runnable_id != $3 AND q.canceled_by IS NULL \ AND s.restart_unless_cancelled", @@ -767,28 +769,6 @@ async fn restart_perpetual_runs_at_path( Ok(true) } -/// The effective token scopes of an earlier job, as the ceiling of a job that re-runs it -/// (a retry, a perpetual restart): the re-run never holds a wider token than the run it -/// replaces. Read from its `job_perms` row, or from `carried` (the scopes it was pulled with) -/// once the monitor has swept that row after the job left the queue. -async fn stored_job_token_scopes( - db: &DB, - job_id: Uuid, - carried: Option<&Vec>, -) -> Result>, Error> { - let row = sqlx::query_scalar!( - "SELECT job_token_scopes FROM job_perms WHERE job_id = $1", - job_id - ) - .fetch_optional(db) - .await?; - Ok(match row { - Some(scopes) => scopes, - // Swept since the job left the queue: the scopes carried from its pull. - None => carried.cloned(), - }) -} - /// A worker older than `job_token_scopes` ignores a step's or agent tool's own restriction, so /// a flow that sets one is refused while such a worker is live, whichever way the flow arrived /// (a deploy, a restart, a preview, a standalone agent, an eval). Free when every worker is @@ -815,6 +795,8 @@ struct PerpetualRunToRestart { /// completion swaps in what a preprocessor returned. preprocessed: Option, args: Option>>>, + /// Its effective token scopes: the replacement never holds a wider token. + job_token_scopes: Option>, } /// What every run at the path moves to. @@ -895,7 +877,7 @@ async fn restart_perpetual_run( // an earlier version the next pass picks up. return Ok(()); } - let scope_ceiling = stored_job_token_scopes(db, run.id, None).await?; + let scope_ceiling = run.job_token_scopes; let (_, tx) = push( db, PushIsolationLevel::Transaction(tx), @@ -1291,7 +1273,7 @@ lazy_static::lazy_static! { pub static ref MAX_RESULT_SIZE_MB: usize = std::env::var("MAX_RESULT_SIZE_MB").unwrap_or("500".to_string()).parse().unwrap_or(500); // Cache for perpetual-restart settings (restart_unless_cancelled, timeout) - keyed by (hash, workspace_id) - static ref RESTART_UNLESS_CANCELLED_CACHE: Cache<(i64, String), (bool, Option)> = Cache::new(10000); + static ref RESTART_UNLESS_CANCELLED_CACHE: Cache<(i64, String), (bool, Option, Option>)> = Cache::new(10000); // Cache for workspace error handler settings with 60s TTL // Key: workspace_id, Value: (error_handler, error_handler_extra_args, error_handler_muted_on_cancel, error_handler_muted_on_user_path, report_to_instance_alerts, expiry_timestamp) @@ -2077,13 +2059,13 @@ async fn restart_job_if_perpetual_inner( ) -> Result<(), Error> { let cache_key = (hash.0, queued_job.workspace_id.clone()); - let (restart, script_timeout) = if let Some(cached) = + let (restart, script_timeout, script_job_token_scopes) = if let Some(cached) = RESTART_UNLESS_CANCELLED_CACHE.get(&cache_key) { cached } else { let row = sqlx::query!( - "SELECT restart_unless_cancelled, timeout FROM script WHERE hash = $1 AND workspace_id = $2", + "SELECT restart_unless_cancelled, timeout, job_token_scopes FROM script WHERE hash = $1 AND workspace_id = $2", hash.0, &queued_job.workspace_id ) @@ -2094,10 +2076,12 @@ async fn restart_job_if_perpetual_inner( .as_ref() .and_then(|r| r.restart_unless_cancelled) .unwrap_or(false); - let script_timeout = row.and_then(|r| r.timeout); + let script_timeout = row.as_ref().and_then(|r| r.timeout); + let script_job_token_scopes = row.and_then(|r| r.job_token_scopes); - RESTART_UNLESS_CANCELLED_CACHE.insert(cache_key, (restart, script_timeout)); - (restart, script_timeout) + let cached = (restart, script_timeout, script_job_token_scopes); + RESTART_UNLESS_CANCELLED_CACHE.insert(cache_key, cached.clone()); + cached }; if restart { @@ -2143,9 +2127,8 @@ async fn restart_job_if_perpetual_inner( .await? .flatten() .unwrap_or_default(); - let scope_ceiling = - stored_job_token_scopes(db, queued_job.id, queued_job.job_token_scopes.as_ref()) - .await?; + // The replacement never holds a wider token than the run it replaces. + let scope_ceiling = queued_job.job_token_scopes.clone(); let (_uuid, tx) = push( db, tx, @@ -2170,16 +2153,7 @@ async fn restart_job_if_perpetual_inner( // TODO(debouncing): handle properly debouncing_settings: DebouncingSettings::default(), labels: None, // labels already set on original job - // Read from the script, not only from the run it replaces: that run's - // `job_perms` row (the ceiling) may already be swept once it left the queue. - job_token_scopes: sqlx::query_scalar!( - "SELECT job_token_scopes FROM script WHERE hash = $1 AND workspace_id = $2", - hash.0, - &queued_job.workspace_id - ) - .fetch_optional(db) - .await? - .flatten(), + job_token_scopes: script_job_token_scopes, }, PushArgs::from(&args.0), &queued_job.created_by, @@ -2465,8 +2439,8 @@ pub async fn maybe_enqueue_native_script_retry( ) .await?; let tx = PushIsolationLevel::IsolatedRoot(db.clone()); - let scope_ceiling = - stored_job_token_scopes(db, job.id, job.job_token_scopes.as_ref()).await?; + // The retry never holds a wider token than the attempt it replaces. + let scope_ceiling = job.job_token_scopes.clone(); let (new_id, mut tx) = match push( db, tx, diff --git a/frontend/src/lib/components/flows/content/FlowRunSettings.svelte b/frontend/src/lib/components/flows/content/FlowRunSettings.svelte index ebaba51557..2d91ab42a7 100644 --- a/frontend/src/lib/components/flows/content/FlowRunSettings.svelte +++ b/frontend/src/lib/components/flows/content/FlowRunSettings.svelte @@ -342,6 +342,12 @@ {/if} + {#if loopSubset} +
+ +
+ {/if} +
- - {#if loopSubset} -
- -
- {/if} diff --git a/frontend/src/lib/components/flows/content/FlowSettings.svelte b/frontend/src/lib/components/flows/content/FlowSettings.svelte index 3fc6ce9908..a86dc289f8 100644 --- a/frontend/src/lib/components/flows/content/FlowSettings.svelte +++ b/frontend/src/lib/components/flows/content/FlowSettings.svelte @@ -460,13 +460,6 @@ }} /> - - {/if} + +