mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 08:16:49 +00:00
fix: batch cancel dropping jobs from other workspaces (#8887)
* fix: batch cancel silently dropping jobs from other workspaces Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: gate cross-workspace batch cancel on all_workspaces flag Enforce path workspace unless all_workspaces=true, so cross-workspace selections only succeed when the UI is actually in all-workspaces mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * sqlx * fix: gate cancel_selection all_workspaces on admins workspace Matches the convention used by count_queue_jobs and count_completed_jobs_detail in the same file — cross-workspace scope is only honored when the path workspace is "admins", preventing clients in regular workspaces from dropping workspace scoping by passing all_workspaces=true. Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
This commit is contained in:
co-authored by
Ruben Fiszel
Claude Opus 4.7
claude[bot]
parent
7dde9824b0
commit
10d1a932d5
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT j.id AS \"id!\", j.workspace_id AS \"workspace_id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2)) AND ($3::text IS NULL OR j.workspace_id = $3)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "945e11f704352945d8937cfd43c023e1a7ea6386cbd3db8c20dbb88c245b28bd"
|
||||
}
|
||||
@@ -11168,6 +11168,10 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: all_workspaces
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
requestBody:
|
||||
description: uuids of the jobs to cancel
|
||||
required: true
|
||||
|
||||
@@ -2085,6 +2085,7 @@ async fn list_queue_jobs(
|
||||
#[derive(Deserialize)]
|
||||
pub struct CancelSelectionQuery {
|
||||
force_cancel: Option<bool>,
|
||||
all_workspaces: Option<bool>,
|
||||
}
|
||||
|
||||
async fn cancel_selection(
|
||||
@@ -2097,23 +2098,53 @@ async fn cancel_selection(
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let tags = get_scope_tags(&authed).map(|v| v.iter().map(|s| s.to_string()).collect_vec());
|
||||
let jobs_to_cancel = sqlx::query_scalar!(
|
||||
"SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))",
|
||||
// Only honor all_workspaces when the path workspace is "admins", matching
|
||||
// the convention used by count_queue_jobs / count_completed_jobs_detail.
|
||||
// Outside the admins workspace, always scope to the path w_id so a client
|
||||
// cannot drop workspace scoping by passing all_workspaces=true.
|
||||
let all_workspaces = w_id == "admins" && query.all_workspaces.unwrap_or(false);
|
||||
let path_w_id = if all_workspaces {
|
||||
None
|
||||
} else {
|
||||
Some(w_id.as_str())
|
||||
};
|
||||
let rows = sqlx::query!(
|
||||
"SELECT j.id AS \"id!\", j.workspace_id AS \"workspace_id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2)) AND ($3::text IS NULL OR j.workspace_id = $3)",
|
||||
&jobs,
|
||||
tags.as_ref().map(|v| v.as_slice())
|
||||
tags.as_ref().map(|v| v.as_slice()),
|
||||
path_w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
cancel_jobs(
|
||||
jobs_to_cancel,
|
||||
&db,
|
||||
authed.username.as_str(),
|
||||
w_id.as_str(),
|
||||
query.force_cancel.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
// cancel_jobs enforces workspace_id per row, so dispatch one call per
|
||||
// workspace so cross-workspace selections (all_workspaces=true) aren't
|
||||
// silently dropped. In single-workspace mode the SQL filter above ensures
|
||||
// this collapses to a single call.
|
||||
let mut jobs_by_workspace: HashMap<String, Vec<Uuid>> = HashMap::new();
|
||||
for row in rows {
|
||||
jobs_by_workspace
|
||||
.entry(row.workspace_id)
|
||||
.or_default()
|
||||
.push(row.id);
|
||||
}
|
||||
|
||||
let force_cancel = query.force_cancel.unwrap_or(false);
|
||||
let mut cancelled = Vec::new();
|
||||
for (workspace_id, ids) in jobs_by_workspace {
|
||||
let Json(mut w_cancelled) = cancel_jobs(
|
||||
ids,
|
||||
&db,
|
||||
authed.username.as_str(),
|
||||
workspace_id.as_str(),
|
||||
force_cancel,
|
||||
)
|
||||
.await?;
|
||||
cancelled.append(&mut w_cancelled);
|
||||
}
|
||||
|
||||
Ok(Json(cancelled))
|
||||
}
|
||||
|
||||
async fn list_filtered_job_uuids(
|
||||
|
||||
@@ -305,7 +305,8 @@
|
||||
const uuids = await JobService.cancelSelection({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: uuidsToCancel,
|
||||
forceCancel: forceCancel
|
||||
forceCancel: forceCancel,
|
||||
allWorkspaces: filters.val.all_workspaces || undefined
|
||||
})
|
||||
selectedIds = []
|
||||
jobsLoader?.loadJobs(true, true)
|
||||
|
||||
Reference in New Issue
Block a user