fix: keep restricted jobs off every dedicated handoff, confine progress flow id

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-02 18:15:29 +02:00
co-authored by Claude Opus 5.5
parent 2ca3f56140
commit 8627b649b4
4 changed files with 55 additions and 6 deletions
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT $2 IN (parent_job, root_job, flow_innermost_root_job) FROM v2_job\n WHERE id = $1 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "67db536d06c57f195641f178ce651cbd7b8896984115d55318bb6f4100d72d9a"
}
+1 -1
View File
@@ -1 +1 @@
7737c32ebed0901cc143ada358654ee4743b02b3
d5879586211e7116d8198f9a03a0fcbb81c55787
@@ -173,10 +173,33 @@ struct JobProgressSetRequest {
}
async fn set_job_progress(
authed: windmill_api_auth::ApiAuthed,
Extension(db): Extension<DB>,
Path((w_id, job_id)): Path<(String, Uuid)>,
Json(JobProgressSetRequest { percent, flow_job_id }): Json<JobProgressSetRequest>,
) -> error::JsonResult<()> {
// A restricted job token reaches this route without a scope only for its own job
// (`is_own_job_runtime_route`), so the flow it reports progress to must be its own too.
if let (Some(flow_job_id), Some(token_job), Some(_)) =
(flow_job_id, authed.job_id, &authed.scopes)
{
let own_flow = sqlx::query_scalar!(
"SELECT $2 IN (parent_job, root_job, flow_innermost_root_job) FROM v2_job
WHERE id = $1 AND workspace_id = $3",
token_job,
flow_job_id,
&w_id
)
.fetch_optional(&db)
.await?
.flatten()
.unwrap_or(false);
if !own_flow {
return Err(error::Error::PermissionDenied(format!(
"flow job {flow_job_id} is not this job's flow"
)));
}
}
// If flow_job_id exists, than we should modify flow_status of corresponding module
// Individual jobs and flows are handled differently
if let Some(flow_job_id) = flow_job_id {
+7 -5
View File
@@ -3595,8 +3595,12 @@ pub async fn run_worker(
// dispatch by path and return before that check, so a job sent down them
// would run with whatever arguments survived the failure.
let fails_before_running = job.pre_run_error.is_some();
// A dedicated worker or flow runner runs every job it gets with its own
// unscoped worker token, so a job with a restricted token runs here, with the
// token minted for it, whichever tag brought it.
let restricted = job.job_token_scopes.is_some();
if !dedicated_workers.is_empty() && !fails_before_running {
if !dedicated_workers.is_empty() && !fails_before_running && !restricted {
let dedicated_worker_tx = job.runnable_path.as_ref().and_then(|path| {
// For flow steps inside branches/loops, runnable_path includes
// nesting segments (e.g. f/flow/branchone-0/a) but the dedicated
@@ -3641,10 +3645,8 @@ pub async fn run_worker(
NextJob::Http(_) => None,
};
// A flow runner runs every step it gets with its own unscoped worker token, so
// a step with a restricted token runs here, with the token minted for it.
if let Some(flow_runners) = flow_runners
.filter(|_| !fails_before_running && job.job_token_scopes.is_none())
if let Some(flow_runners) =
flow_runners.filter(|_| !fails_before_running && !restricted)
{
let key_o = job.flow_step_id.as_ref().map(|x| x.to_string());
if let Some(key) = key_o {