From 43732d2fb5a2f1fcb7c2f5ddef45024dd8747813 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 18 Dec 2022 12:22:04 +0100 Subject: [PATCH] require ownership to move items && automatically edit schedule paths --- backend/sqlx-data.json | 536 ++++++++++++++---- backend/windmill-api/openapi.yaml | 4 - backend/windmill-api/src/apps.rs | 8 +- backend/windmill-api/src/flows.rs | 46 +- backend/windmill-api/src/resources.rs | 6 +- backend/windmill-api/src/schedule.rs | 292 +++++++++- backend/windmill-api/src/scripts.rs | 175 +++--- backend/windmill-api/src/users.rs | 25 + backend/windmill-api/src/variables.rs | 6 +- backend/windmill-common/src/lib.rs | 1 + backend/windmill-common/src/schedule.rs | 28 + backend/windmill-queue/src/schedule.rs | 318 +---------- backend/windmill-worker/src/jobs.rs | 4 +- frontend/src/lib/components/CronInput.svelte | 102 ++-- .../src/lib/components/FlowBuilder.svelte | 14 +- frontend/src/lib/components/Path.svelte | 72 +-- .../src/lib/components/ResourceEditor.svelte | 26 +- .../toggleButton/ToggleButtonGroup.svelte | 2 +- .../flows/map/InsertModuleButton.svelte | 6 +- .../src/lib/components/home/ItemsList.svelte | 4 +- .../components/sidebar/SidebarContent.svelte | 2 +- .../lib/components/sidebar/UserMenu.svelte | 2 +- frontend/src/routes/audit_logs.svelte | 11 +- frontend/src/routes/resources.svelte | 4 +- frontend/src/routes/schedule/add.svelte | 69 ++- .../src/routes/scripts/get/[...hash].svelte | 59 +- frontend/src/routes/workspace_settings.svelte | 2 +- 27 files changed, 1119 insertions(+), 705 deletions(-) create mode 100644 backend/windmill-common/src/schedule.rs diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 961c7020a0..e267888b62 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -487,6 +487,35 @@ }, "query": "SELECT * from workspace_invite WHERE email = $1" }, + "1b9a418e1a6bda6abde432112a4e94bfef3708a57a8fa31670a61e001aaf2fd6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies" + ] + }, + "name": "job_kind" + } + } + ] + } + }, + "query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND job_kind = $2" + }, "1bf2ca894246bd716875635b2d0c294a1ce2ed21916097ea165df240f7421a1e": { "describe": { "columns": [], @@ -1067,18 +1096,6 @@ }, "query": "UPDATE app SET versions = array_append(versions, $1) WHERE id = $2" }, - "3fabb3857c9cf2d057814b54ee54a95d01b6a7d9e89bea239b832a9d70f0044b": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false" - }, "453501fbd61efd26647baf9b6ef702ce0bc2e920914e9f08fe5f2a5f4ab03f02": { "describe": { "columns": [ @@ -1189,103 +1206,6 @@ }, "query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['step'], $1)\n WHERE id = $2\n " }, - "4eb6b80c410e00e8a72eced6c25cdd3ed941e7a46a0619173f272eec7f28a3c1": { - "describe": { - "columns": [ - { - "name": "workspace_id", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "path", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "edited_by", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "edited_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "schedule", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "offset_", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "enabled", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "script_path", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "args", - "ordinal": 8, - "type_info": "Jsonb" - }, - { - "name": "extra_perms", - "ordinal": 9, - "type_info": "Jsonb" - }, - { - "name": "is_flow", - "ordinal": 10, - "type_info": "Bool" - }, - { - "name": "email", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "error", - "ordinal": 12, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Bool", - "Jsonb", - "Text", - "Text" - ] - } - }, - "query": "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *" - }, "5061c0d054bf4f028e7fe51a8f9389024c6ae4492755cadac0f7167e5300bda0": { "describe": { "columns": [], @@ -1557,6 +1477,122 @@ }, "query": "SELECT app.id, app.path, app.summary, app.versions, app.policy,\n app.extra_perms, app_version.value, \n app_version.created_at, app_version.created_by from app, app_version \n WHERE app_version.id = $1 AND app.id = app_version.flow_id AND app.workspace_id = $2" }, + "55960eb3cd5a82f593496349a2521607f8066d88ff11a2ca4a63b6e37e62b71b": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "offset_", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "enabled", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 9, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "email", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "error", + "ordinal": 12, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Varchar", + "Jsonb", + "Text", + "Text" + ] + } + }, + "query": "UPDATE schedule SET schedule = $1, args = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *" + }, + "56839d3aec6c0177d14589aedda8d5c431d841b6d5d0d99ce3836bb42d4d83d9": { + "describe": { + "columns": [ + { + "name": "is_flow", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT is_flow FROM schedule WHERE path = $1 AND workspace_id = $2" + }, "576b00c515ee7cbf628b1881596d8a03c1c506b309e39a0048a3a8fe18f37070": { "describe": { "columns": [], @@ -2772,6 +2808,100 @@ }, "query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1" }, + "911b1e1f2a5ba6d5159916e5598020e680c45043b0736ad0153ee261a151dd90": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "offset_", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "enabled", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 9, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "email", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "error", + "ordinal": 12, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text" + ] + } + }, + "query": "UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *" + }, "924e1d5cbb682db0d6121dd911f38cf8948d0e59ca47428daa4f43fce686ba80": { "describe": { "columns": [ @@ -3692,6 +3822,100 @@ }, "query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2" }, + "bfc760c2f3f1477b177a7c570ddadda818dd3f57142165f381c1db01c26cf1bf": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "offset_", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "enabled", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 9, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "email", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "error", + "ordinal": 12, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text" + ] + } + }, + "query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *" + }, "c07577e99ef7817473b7f8cef2a4d97e9867260f932aeed2755964fab4a4e0de": { "describe": { "columns": [], @@ -4518,6 +4742,100 @@ }, "query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator" }, + "eced0a09ba547ce1dccb54a5419b22373603c9d01f77047b3553bde125bf71e8": { + "describe": { + "columns": [ + { + "name": "workspace_id", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "path", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "edited_by", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "edited_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "schedule", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "offset_", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "enabled", + "ordinal": 6, + "type_info": "Bool" + }, + { + "name": "script_path", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "args", + "ordinal": 8, + "type_info": "Jsonb" + }, + { + "name": "extra_perms", + "ordinal": 9, + "type_info": "Jsonb" + }, + { + "name": "is_flow", + "ordinal": 10, + "type_info": "Bool" + }, + { + "name": "email", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "error", + "ordinal": 12, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text" + ] + } + }, + "query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *" + }, "ed400dbf4a1fa597bc984240cfe9069369a8a265b127116187d78c0ba93ae076": { "describe": { "columns": [ diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1bcd30534d..658d25031a 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4806,10 +4806,6 @@ components: properties: schedule: type: string - script_path: - type: string - is_flow: - type: boolean args: $ref: "#/components/schemas/ScriptArgs" required: diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 724b8f3d12..c143e54a0b 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use crate::{ db::{UserDB, DB}, jobs::script_path_to_payload, - users::{Authed, OptAuthed}, + users::{require_owner_of_path, Authed, OptAuthed}, }; use axum::{ extract::{Extension, Path, Query}, @@ -309,6 +309,7 @@ async fn delete_app( async fn update_app( authed: Authed, Extension(user_db): Extension, + Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, ) -> Result { @@ -324,6 +325,11 @@ async fn update_app( sqlb.and_where_eq("workspace_id", "?".bind(&w_id)); if let Some(npath) = &ns.path { + if npath != path { + if !authed.is_admin { + require_owner_of_path(&w_id, &authed.username, &path, &db).await?; + } + } sqlb.set_str("path", npath); } diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index a02e93b731..cf940cb18d 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -21,15 +21,17 @@ use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ error::{self, to_anyhow, Error, JsonResult, Result}, flows::{Flow, ListFlowQuery, ListableFlow, NewFlow}, + schedule::Schedule, utils::{ http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath, }, }; -use windmill_queue::{push, JobPayload}; +use windmill_queue::{push, schedule::push_scheduled_job, JobPayload}; use crate::{ db::{UserDB, DB}, - users::Authed, + schedule::clear_schedule, + users::{require_owner_of_path, Authed}, }; pub fn workspaced_service() -> Router { @@ -238,6 +240,7 @@ async fn check_schedule_conflict<'c>( async fn update_flow( authed: Authed, Extension(user_db): Extension, + Extension(db): Extension, Path((w_id, flow_path)): Path<(String, StripPath)>, Json(nf): Json, ) -> Result { @@ -249,7 +252,7 @@ async fn update_flow( let schema = nf.schema.map(|x| x.0); let old_dep_job = sqlx::query_scalar!( "SELECT dependency_job FROM flow WHERE path = $1 AND workspace_id = $2", - nf.path, + flow_path, w_id ) .fetch_optional(&mut tx) @@ -270,6 +273,43 @@ async fn update_flow( .execute(&mut tx) .await?; + if nf.path != flow_path { + if !authed.is_admin { + require_owner_of_path(&w_id, &authed.username, &flow_path, &db).await?; + } + + let mut schedulables = sqlx::query_as!( + Schedule, + "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *", + nf.path, + flow_path, + w_id, + ) + .fetch_all(&mut tx) + .await?; + + let schedule = sqlx::query_as!(Schedule, + "UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *", + nf.path, + flow_path, + w_id, + ) + .fetch_optional(&mut tx) + .await?; + + if let Some(schedule) = schedule { + schedulables.push(schedule); + } + + for schedule in schedulables { + clear_schedule(&mut tx, flow_path, true).await?; + + if schedule.enabled { + tx = push_scheduled_job(tx, schedule).await?; + } + } + } + audit_log( &mut tx, &authed.username, diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index c56da5a3a1..ab35e2c3a4 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -8,7 +8,7 @@ use crate::{ db::{UserDB, DB}, - users::Authed, + users::{require_owner_of_path, Authed}, }; use axum::{ extract::{Extension, Path, Query}, @@ -315,6 +315,7 @@ async fn delete_resource( async fn update_resource( authed: Authed, Extension(user_db): Extension, + Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, ) -> Result { @@ -346,6 +347,9 @@ async fn update_resource( let npath = not_found_if_none(npath_o, "Resource", path)?; if let Some(npath) = ns.path { + if !authed.is_admin { + require_owner_of_path(&w_id, &authed.username, &path, &db).await?; + } sqlx::query!( "UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3", npath, diff --git a/backend/windmill-api/src/schedule.rs b/backend/windmill-api/src/schedule.rs index 7ffc351ee8..262c4aa8ca 100644 --- a/backend/windmill-api/src/schedule.rs +++ b/backend/windmill-api/src/schedule.rs @@ -6,6 +6,8 @@ * LICENSE-AGPL for a copy of the license. */ +use std::str::FromStr; + use crate::{ db::{UserDB, DB}, users::Authed, @@ -15,15 +17,16 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; -use chrono::DateTime; +use chrono::{DateTime, FixedOffset}; +use serde::Deserialize; +use sqlx::{Postgres, Transaction}; +use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ - error::{JsonResult, Result}, - utils::{not_found_if_none, Pagination, StripPath}, -}; -use windmill_queue::{ - self, - schedule::{EditSchedule, NewSchedule, PreviewPayload, Schedule, SetEnabled}, + error::{Error, JsonResult, Result}, + schedule::Schedule, + utils::{not_found_if_none, paginate, Pagination, StripPath}, }; +use windmill_queue::{self, schedule::push_scheduled_job, JobKind}; pub fn workspaced_service() -> Router { Router::new() @@ -40,17 +43,71 @@ pub fn global_service() -> Router { Router::new().route("/preview", post(preview_schedule)) } +#[derive(Deserialize)] +pub struct NewSchedule { + pub path: String, + pub schedule: String, + pub offset: i32, + pub script_path: String, + pub is_flow: bool, + pub args: Option, + pub enabled: Option, +} + async fn create_schedule( authed: Authed, Extension(user_db): Extension, Path(w_id): Path, Json(ns): Json, ) -> Result { - let tx = user_db.begin(&authed).await?; - let res = - windmill_queue::schedule::create_schedule(tx, w_id, ns, &authed.username, &authed.email) - .await?; - Ok(res) + let mut tx = user_db.begin(&authed).await?; + cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?; + check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?; + + let schedule = sqlx::query_as!( + Schedule, + "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \ + is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *", + w_id, + ns.path, + ns.schedule, + ns.offset, + &authed.username, + ns.script_path, + ns.is_flow, + ns.args, + ns.enabled.unwrap_or(false), + &authed.email + ) + .fetch_one(&mut tx) + .await + .map_err(|e| Error::InternalErr(format!("inserting schedule in {w_id}: {e}")))?; + + audit_log( + &mut tx, + &authed.username, + "schedule.create", + ActionKind::Create, + &w_id, + Some(&ns.path.to_string()), + Some( + [ + Some(("schedule", ns.schedule.as_str())), + Some(("script_path", ns.script_path.as_str())), + ] + .into_iter() + .flatten() + .collect(), + ), + ) + .await?; + + if ns.enabled.unwrap_or(true) { + tx = push_scheduled_job(tx, schedule).await? + } + tx.commit().await?; + + Ok(ns.path.to_string()) } async fn edit_schedule( @@ -59,9 +116,55 @@ async fn edit_schedule( Path((w_id, path)): Path<(String, StripPath)>, Json(es): Json, ) -> Result { - let tx = user_db.begin(&authed).await?; - let res = windmill_queue::schedule::edit_schedule(tx, w_id, path, es, &authed.username).await?; - Ok(res) + let mut tx = user_db.begin(&authed).await?; + let path = path.to_path(); + + cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?; + + let is_flow = sqlx::query_scalar!( + "SELECT is_flow FROM schedule WHERE path = $1 AND workspace_id = $2", + path, + w_id + ) + .fetch_one(&mut tx) + .await?; + + clear_schedule(&mut tx, path, is_flow).await?; + let schedule = sqlx::query_as!( + Schedule, + "UPDATE schedule SET schedule = $1, args = $2 WHERE path \ + = $3 AND workspace_id = $4 RETURNING *", + es.schedule, + es.args, + path, + w_id, + ) + .fetch_one(&mut tx) + .await + .map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?; + + if schedule.enabled { + tx = push_scheduled_job(tx, schedule).await?; + } + + audit_log( + &mut tx, + &authed.username, + "schedule.edit", + ActionKind::Update, + &w_id, + Some(&path.to_string()), + Some( + [Some(("schedule", es.schedule.as_str()))] + .into_iter() + .flatten() + .collect(), + ), + ) + .await?; + tx.commit().await?; + + Ok(path.to_string()) } async fn list_schedule( @@ -70,9 +173,19 @@ async fn list_schedule( Path(w_id): Path, Query(pagination): Query, ) -> JsonResult> { - let tx = user_db.begin(&authed).await?; - let res = windmill_queue::schedule::list_schedule(tx, w_id, pagination).await?; - Ok(Json(res)) + let mut tx = user_db.begin(&authed).await?; + let (per_page, offset) = paginate(pagination); + let rows = sqlx::query_as!( + Schedule, + "SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3", + w_id, + per_page as i64, + offset as i64 + ) + .fetch_all(&mut tx) + .await?; + tx.commit().await?; + Ok(Json(rows)) } async fn get_schedule( @@ -102,7 +215,15 @@ async fn exists_schedule( pub async fn preview_schedule( Json(payload): Json, ) -> JsonResult>> { - Ok(Json(windmill_queue::schedule::preview_schedule(payload)?)) + let schedule = cron::Schedule::from_str(&payload.schedule) + .map_err(|e| Error::BadRequest(e.to_string()))?; + let upcoming: Vec> = schedule + .upcoming(get_offset(payload.offset)) + .take(10) + .map(|x| x.into()) + .collect(); + + Ok(Json(upcoming)) } pub async fn set_enabled( @@ -111,17 +232,41 @@ pub async fn set_enabled( Path((w_id, path)): Path<(String, StripPath)>, Json(payload): Json, ) -> Result { - let tx = user_db.begin(&authed).await?; - let res = windmill_queue::schedule::set_enabled( - tx, - w_id, + let mut tx = user_db.begin(&authed).await?; + let path = path.to_path(); + let schedule_o = sqlx::query_as!( + Schedule, + "UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *", + &payload.enabled, + authed.email, path, - payload, + w_id + ) + .fetch_optional(&mut tx) + .await?; + + let schedule = not_found_if_none(schedule_o, "Schedule", path)?; + + clear_schedule(&mut tx, path, schedule.is_flow).await?; + + if payload.enabled { + tx = push_scheduled_job(tx, schedule).await?; + } + audit_log( + &mut tx, &authed.username, - &authed.email, + "schedule.setenabled", + ActionKind::Update, + &w_id, + Some(path), + Some([("enabled", payload.enabled.to_string().as_ref())].into()), ) .await?; - Ok(res) + tx.commit().await?; + Ok(format!( + "succesfully updated schedule at path {} to status {}", + path, payload.enabled + )) } async fn delete_schedule( @@ -129,7 +274,96 @@ async fn delete_schedule( Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, ) -> Result { - let tx = user_db.begin(&authed).await?; - let res = windmill_queue::schedule::delete_schedule(tx, w_id, path, &authed.username).await?; - Ok(res) + let mut tx = user_db.begin(&authed).await?; + let path = path.to_path(); + + sqlx::query!( + "DELETE FROM schedule WHERE path = $1 AND workspace_id = $2", + path, + w_id + ) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &authed.username, + "schedule.delete", + ActionKind::Delete, + &w_id, + Some(path), + None, + ) + .await?; + + tx.commit().await?; + + Ok(format!("schedule {} deleted", path)) +} + +async fn check_flow_conflict<'c>( + tx: &mut Transaction<'c, Postgres>, + w_id: &str, + path: &str, + is_flow: bool, + script_path: &str, +) -> Result<()> { + if path != script_path || !is_flow { + let exists_flow = sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)", + path, + w_id + ) + .fetch_one(tx) + .await? + .unwrap_or(false); + if exists_flow { + return Err(Error::BadRequest(format!( + "The path is the same as a flow, it can only trigger that flow. + However the provided path is: {script_path} and is_flow is {is_flow}" + ))); + }; + } + Ok(()) +} + +#[derive(Deserialize)] +pub struct EditSchedule { + pub schedule: String, + pub args: Option, +} + +pub async fn clear_schedule<'c>( + db: &mut Transaction<'c, Postgres>, + path: &str, + is_flow: bool, +) -> Result<()> { + let job_kind = if is_flow { + JobKind::Flow + } else { + JobKind::Script + }; + sqlx::query!( + "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND job_kind = $2", + path, + job_kind: JobKind + ) + .execute(db) + .await?; + Ok(()) +} + +#[derive(Deserialize)] +pub struct PreviewPayload { + pub schedule: String, + pub offset: Option, +} + +fn get_offset(offset: Option) -> FixedOffset { + FixedOffset::west(offset.unwrap_or(0) * 60) +} + +#[derive(Deserialize)] +pub struct SetEnabled { + pub enabled: bool, } diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 2a9f453928..391a74f497 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -12,7 +12,8 @@ use windmill_audit::{audit_log, ActionKind}; use crate::{ db::{UserDB, DB}, - users::Authed, + schedule::clear_schedule, + users::{require_owner_of_path, Authed}, }; use axum::{ extract::{Extension, Path, Query}, @@ -30,6 +31,7 @@ use std::{ }; use windmill_common::{ error::{Error, JsonResult, Result}, + schedule::Schedule, scripts::{ to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Script, ScriptHash, ScriptKind, ScriptLang, @@ -39,7 +41,7 @@ use windmill_common::{ list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath, }, }; -use windmill_queue; +use windmill_queue::{self, schedule::push_scheduled_job}; const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20; @@ -180,6 +182,7 @@ fn hash_script(ns: &NewScript) -> i64 { async fn create_script( authed: Authed, Extension(user_db): Extension, + Extension(db): Extension, Path(w_id): Path, Json(ns): Json, ) -> Result<(StatusCode, String)> { @@ -210,83 +213,95 @@ async fn create_script( .fetch_optional(&mut tx) .await?; - let parent_hashes_and_perms: Option<(Vec, serde_json::Value)> = - match (&ns.parent_hash, clashing_script) { - (None, None) => Ok(None), - (None, Some(s)) => Err(Error::BadRequest(format!( - "Path conflict for {} with non-archived hash {}", - &ns.path, &s.hash - ))), - (Some(p_hash), o) => { - if sqlx::query_scalar!( - "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2", - p_hash.0, - &w_id - ) - .fetch_optional(&mut tx) - .await? - .is_none() - { - return Err(Error::BadRequest( - "The parent hash does not seem to exist".to_owned(), - )); - }; + struct ParentInfo { + p_hashes: Vec, + perms: serde_json::Value, + p_path: String, + } + let parent_hashes_and_perms: Option = match (&ns.parent_hash, clashing_script) { + (None, None) => Ok(None), + (None, Some(s)) => Err(Error::BadRequest(format!( + "Path conflict for {} with non-archived hash {}", + &ns.path, &s.hash + ))), + (Some(p_hash), o) => { + if sqlx::query_scalar!( + "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2", + p_hash.0, + &w_id + ) + .fetch_optional(&mut tx) + .await? + .is_none() + { + return Err(Error::BadRequest( + "The parent hash does not seem to exist".to_owned(), + )); + }; - let clashing_hash_o = sqlx::query_scalar!( - "SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2", - p_hash.0, - &w_id - ) - .fetch_optional(&mut tx) - .await?; + let clashing_hash_o = sqlx::query_scalar!( + "SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2", + p_hash.0, + &w_id + ) + .fetch_optional(&mut tx) + .await?; - if let Some(clashing_hash) = clashing_hash_o { - return Err(Error::BadRequest(format!( - "A script with hash {} with same parent_hash has been found. However, the \ + if let Some(clashing_hash) = clashing_hash_o { + return Err(Error::BadRequest(format!( + "A script with hash {} with same parent_hash has been found. However, the \ lineage must be linear: no 2 scripts can have the same parent", - ScriptHash(clashing_hash) - ))); - }; + ScriptHash(clashing_hash) + ))); + }; - let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?; + let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?; - let ph = { - let v = ps.parent_hashes.map(|x| x.0).unwrap_or_default(); - let mut v: Vec = v - .into_iter() - .take(MAX_HASH_HISTORY_LENGTH_STORED - 1) - .collect(); - v.insert(0, p_hash.0); - v - }; - let r: Result, serde_json::Value)>> = match o { - Some(clashing_script) - if clashing_script.path == ns.path - && clashing_script.hash.0 != p_hash.0 => - { - Err(Error::BadRequest(format!( - "Path conflict for {} with non-archived hash {}", - &ns.path, &clashing_script.hash - ))) - } - Some(_) => Ok(Some((ph, ps.extra_perms))), - None => Ok(Some((ph, ps.extra_perms))), - }; - sqlx::query!( - "UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2", - p_hash.0, - &w_id - ) - .execute(&mut tx) - .await?; - r + if ps.path != ns.path { + if !authed.is_admin { + require_owner_of_path(&w_id, &authed.username, &ps.path, &db).await?; + } } - }?; - let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.0[..]); + let ph = { + let v = ps.parent_hashes.map(|x| x.0).unwrap_or_default(); + let mut v: Vec = v + .into_iter() + .take(MAX_HASH_HISTORY_LENGTH_STORED - 1) + .collect(); + v.insert(0, p_hash.0); + v + }; + let r: Result> = match o { + Some(clashing_script) + if clashing_script.path == ns.path && clashing_script.hash.0 != p_hash.0 => + { + Err(Error::BadRequest(format!( + "Path conflict for {} with non-archived hash {}", + &ns.path, &clashing_script.hash + ))) + } + Some(_) | None => Ok(Some(ParentInfo { + p_hashes: ph, + perms: ps.extra_perms, + p_path: ps.path, + })), + }; + sqlx::query!( + "UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2", + p_hash.0, + &w_id + ) + .execute(&mut tx) + .await?; + r + } + }?; + + let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.p_hashes[..]); let extra_perms = parent_hashes_and_perms .as_ref() - .map(|v| v.1.clone()) + .map(|v| v.perms.clone()) .unwrap_or(json!({})); let lock = if ns.language == ScriptLang::Bash || ns.language == ScriptLang::Deno { @@ -322,6 +337,26 @@ async fn create_script( .execute(&mut tx) .await?; + if let Some(p_path) = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone()) { + let schedulables = sqlx::query_as!( + Schedule, + "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *", + ns.path, + p_path, + w_id, + ) + .fetch_all(&mut tx) + .await?; + + for schedule in schedulables { + clear_schedule(&mut tx, &schedule.path, false).await?; + + if schedule.enabled { + tx = push_scheduled_job(tx, schedule).await?; + } + } + } + let mut tx = if needs_lock_gen { let dependencies = match ns.language { ScriptLang::Python3 => { diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index 4641462ce7..4a1642cbdf 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -782,6 +782,31 @@ pub async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result< Ok(groups) } +pub async fn require_owner_of_path(w_id: &str, username: &str, path: &str, db: &DB) -> Result<()> { + let splitted = path.split("/").collect::>(); + if splitted[0] == "u" { + if splitted[1] == username { + return Ok(()); + } else { + return Err(Error::BadRequest(format!( + "only the owner {} is authorized to perform this operation", + splitted[1] + ))); + } + } else if splitted[0] == "g" { + let groups = get_groups_for_user(w_id, username, db).await?; + if groups.contains(&username.to_string()) { + return Ok(()); + } else { + return Err(Error::BadRequest(format!( + "{} is not a member of {} and hence is not authorized to perform this operation", + username, splitted[1] + ))); + } + } + Err(Error::BadRequest(format!("not recognized owner kind"))) +} + async fn whois( Extension(db): Extension, Path((w_id, username)): Path<(String, String)>, diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 4bdc545a38..54a93ccf94 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use crate::{ db::{UserDB, DB}, oauth2::{AllClients, _refresh_token}, - users::Authed, + users::{require_owner_of_path, Authed}, BaseUrl, }; /* @@ -297,6 +297,7 @@ struct EditVariable { async fn update_variable( authed: Authed, Extension(user_db): Extension, + Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, ) -> Result { @@ -351,6 +352,9 @@ async fn update_variable( let npath_o: Option = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?; if let Some(npath) = ns.path { + if !authed.is_admin { + require_owner_of_path(&w_id, &authed.username, &path, &db).await?; + } sqlx::query!( "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3", npath, diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index bbc2b1b80b..d94efa4801 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -17,6 +17,7 @@ pub mod flow_status; pub mod flows; pub mod more_serde; pub mod oauth2; +pub mod schedule; pub mod scripts; pub mod users; pub mod utils; diff --git a/backend/windmill-common/src/schedule.rs b/backend/windmill-common/src/schedule.rs new file mode 100644 index 0000000000..95089f1081 --- /dev/null +++ b/backend/windmill-common/src/schedule.rs @@ -0,0 +1,28 @@ +/* + * Author: Ruben Fiszel + * Copyright: Windmill Labs, Inc 2022 + * This file and its contents are licensed under the AGPLv3 License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use chrono::DateTime; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +#[derive(FromRow, Serialize, Deserialize, Debug)] +pub struct Schedule { + pub workspace_id: String, + pub path: String, + pub edited_by: String, + pub edited_at: DateTime, + pub schedule: String, + pub offset_: i32, + pub enabled: bool, + pub script_path: String, + pub is_flow: bool, + pub args: Option, + pub extra_perms: serde_json::Value, + pub email: String, + pub error: Option, +} diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index 3ed9205f37..790979cbf2 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -8,45 +8,16 @@ use std::str::FromStr; -use chrono::{DateTime, Duration, FixedOffset}; -use serde::{Deserialize, Serialize}; -use sqlx::{query_scalar, FromRow, Postgres, Transaction}; -use windmill_audit::{audit_log, ActionKind}; +use chrono::Duration; +use sqlx::{query_scalar, Postgres, Transaction}; use windmill_common::{ - error::{self, Error, Result}, - utils::{get_owner_from_path, not_found_if_none, now_from_db, paginate, Pagination, StripPath}, + error::{self, Result}, + schedule::Schedule, + utils::{get_owner_from_path, now_from_db, StripPath}, }; use crate::{push, JobPayload}; -#[derive(FromRow, Serialize, Deserialize, Debug)] -pub struct Schedule { - pub workspace_id: String, - pub path: String, - pub edited_by: String, - pub edited_at: DateTime, - pub schedule: String, - pub offset_: i32, - pub enabled: bool, - pub script_path: String, - pub is_flow: bool, - pub args: Option, - pub extra_perms: serde_json::Value, - pub email: String, - pub error: Option, -} - -#[derive(Deserialize)] -pub struct NewSchedule { - pub path: String, - pub schedule: String, - pub offset: i32, - pub script_path: String, - pub is_flow: bool, - pub args: Option, - pub enabled: Option, -} - pub async fn push_scheduled_job<'c>( mut tx: Transaction<'c, Postgres>, schedule: Schedule, @@ -129,181 +100,6 @@ pub async fn push_scheduled_job<'c>( Ok(tx) } -pub async fn create_schedule( - mut tx: Transaction<'_, Postgres>, - w_id: String, - ns: NewSchedule, - username: &str, - email: &str, -) -> Result { - cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; - check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?; - - let schedule = sqlx::query_as!( - Schedule, - "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \ - is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *", - w_id, - ns.path, - ns.schedule, - ns.offset, - username, - ns.script_path, - ns.is_flow, - ns.args, - ns.enabled.unwrap_or(false), - email - ) - .fetch_one(&mut tx) - .await - .map_err(|e| Error::InternalErr(format!("inserting schedule in {w_id}: {e}")))?; - - audit_log( - &mut tx, - username, - "schedule.create", - ActionKind::Create, - &w_id, - Some(&ns.path.to_string()), - Some( - [ - Some(("schedule", ns.schedule.as_str())), - Some(("script_path", ns.script_path.as_str())), - ] - .into_iter() - .flatten() - .collect(), - ), - ) - .await?; - - if ns.enabled.unwrap_or(true) { - tx = push_scheduled_job(tx, schedule).await? - } - tx.commit().await?; - - Ok(ns.path.to_string()) -} - -async fn check_flow_conflict<'c>( - tx: &mut Transaction<'c, Postgres>, - w_id: &str, - path: &str, - is_flow: bool, - script_path: &str, -) -> error::Result<()> { - if path != script_path || !is_flow { - let exists_flow = sqlx::query_scalar!( - "SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)", - path, - w_id - ) - .fetch_one(tx) - .await? - .unwrap_or(false); - if exists_flow { - return Err(error::Error::BadConfig(format!( - "The path is the same as a flow, it can only trigger that flow. - However the provided path is: {script_path} and is_flow is {is_flow}" - ))); - }; - } - Ok(()) -} - -#[derive(Deserialize)] -pub struct EditSchedule { - pub schedule: String, - pub script_path: String, - pub is_flow: bool, - pub args: Option, -} - -async fn clear_schedule<'c>(db: &mut Transaction<'c, Postgres>, path: &str) -> Result<()> { - sqlx::query!( - "DELETE FROM queue WHERE schedule_path = $1 AND running = false", - path - ) - .execute(db) - .await?; - Ok(()) -} - -pub async fn edit_schedule( - mut tx: Transaction<'_, Postgres>, - w_id: String, - path: StripPath, - es: EditSchedule, - username: &String, -) -> Result { - let path = path.to_path(); - - cron::Schedule::from_str(&es.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; - - check_flow_conflict(&mut tx, &w_id, &path, es.is_flow, &es.script_path).await?; - - clear_schedule(&mut tx, path).await?; - let schedule = sqlx::query_as!( - Schedule, - "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path \ - = $5 AND workspace_id = $6 RETURNING *", - es.schedule, - es.script_path, - es.is_flow, - es.args, - path, - w_id, - ) - .fetch_one(&mut tx) - .await - .map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?; - - if schedule.enabled { - tx = push_scheduled_job(tx, schedule).await?; - } - - audit_log( - &mut tx, - username, - "schedule.edit", - ActionKind::Update, - &w_id, - Some(&path.to_string()), - Some( - [ - Some(("schedule", es.schedule.as_str())), - Some(("script_path", es.script_path.as_str())), - ] - .into_iter() - .flatten() - .collect(), - ), - ) - .await?; - tx.commit().await?; - - Ok(path.to_string()) -} - -pub async fn list_schedule( - mut tx: Transaction<'_, Postgres>, - w_id: String, - pagination: Pagination, -) -> Result> { - let (per_page, offset) = paginate(pagination); - let rows = sqlx::query_as!( - Schedule, - "SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3", - w_id, - per_page as i64, - offset as i64 - ) - .fetch_all(&mut tx) - .await?; - tx.commit().await?; - Ok(rows) -} - pub async fn get_schedule_opt<'c>( db: &mut Transaction<'c, Postgres>, w_id: &str, @@ -339,110 +135,6 @@ pub async fn exists_schedule( Ok(exists) } -#[derive(Deserialize)] -pub struct PreviewPayload { - pub schedule: String, - pub offset: Option, -} - -pub fn preview_schedule( - PreviewPayload { schedule, offset }: PreviewPayload, -) -> Result>> { - let schedule = - cron::Schedule::from_str(&schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; - let upcoming: Vec> = schedule - .upcoming(get_offset(offset)) - .take(10) - .map(|x| x.into()) - .collect(); - Ok(upcoming) -} - -fn get_offset(offset: Option) -> FixedOffset { - FixedOffset::west(offset.unwrap_or(0) * 60) -} - -#[derive(Deserialize)] -pub struct SetEnabled { - pub enabled: bool, -} - -pub async fn set_enabled( - mut tx: Transaction<'_, Postgres>, - w_id: String, - path: StripPath, - SetEnabled { enabled }: SetEnabled, - username: &str, - email: &str, -) -> Result { - let path = path.to_path(); - let schedule_o = sqlx::query_as!( - Schedule, - "UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *", - enabled, - email, - path, - w_id - ) - .fetch_optional(&mut tx) - .await?; - - let schedule = not_found_if_none(schedule_o, "Schedule", path)?; - - clear_schedule(&mut tx, path).await?; - - if enabled { - tx = push_scheduled_job(tx, schedule).await?; - } - audit_log( - &mut tx, - username, - "schedule.setenabled", - ActionKind::Update, - &w_id, - Some(path), - Some([("enabled", enabled.to_string().as_ref())].into()), - ) - .await?; - tx.commit().await?; - Ok(format!( - "succesfully updated schedule at path {} to status {}", - path, enabled - )) -} - -pub async fn delete_schedule( - mut tx: Transaction<'_, Postgres>, - w_id: String, - path: StripPath, - username: &str, -) -> Result { - let path = path.to_path(); - - sqlx::query!( - "DELETE FROM schedule WHERE path = $1 AND workspace_id = $2", - path, - w_id - ) - .execute(&mut tx) - .await?; - - audit_log( - &mut tx, - username, - "schedule.delete", - ActionKind::Delete, - &w_id, - Some(path), - None, - ) - .await?; - - tx.commit().await?; - - Ok(format!("schedule {} deleted", path)) -} - fn schedule_to_user(path: &str) -> String { format!("schedule-{}", path.replace('/', "-")) } diff --git a/backend/windmill-worker/src/jobs.rs b/backend/windmill-worker/src/jobs.rs index 34b28001ca..2bc1c67b6f 100644 --- a/backend/windmill-worker/src/jobs.rs +++ b/backend/windmill-worker/src/jobs.rs @@ -10,7 +10,7 @@ use serde_json::{Map, Value}; use sqlx::{Pool, Postgres}; use tracing::instrument; use uuid::Uuid; -use windmill_common::{error::Error, flow_status::FlowStatusModule}; +use windmill_common::{error::Error, flow_status::FlowStatusModule, schedule::Schedule}; use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob}; #[instrument(level = "trace", skip_all)] @@ -238,7 +238,7 @@ pub async fn schedule_again_if_scheduled( if schedule.enabled && script_path == schedule.script_path { let res = windmill_queue::schedule::push_scheduled_job( tx, - windmill_queue::schedule::Schedule { + Schedule { workspace_id: w_id.to_owned(), path: schedule.path.clone(), edited_by: schedule.edited_by, diff --git a/frontend/src/lib/components/CronInput.svelte b/frontend/src/lib/components/CronInput.svelte index e3cced8a58..b7a18808c5 100644 --- a/frontend/src/lib/components/CronInput.svelte +++ b/frontend/src/lib/components/CronInput.svelte @@ -35,56 +35,58 @@ } -
{cronError}
-
- - -
-{#if !disabled} -
- - - +
+
{cronError}
+
+ +
-{/if} - - - {#if preview && preview.length > 0} -
-
The next runs will be scheduled at:
-
    - {#each preview.slice(0, limit) as p} -
  • {displayDate(p, true)}
  • - {/each} -
  • ...
  • - {#if limit != 10} - - {:else} - - {/if} -
+ {#if !disabled} +
+ + +
{/if} - + + + {#if preview && preview.length > 0} +
+
The next runs will be scheduled at:
+
    + {#each preview.slice(0, limit) as p} +
  • {displayDate(p, true)}
  • + {/each} +
  • ...
  • + {#if limit != 10} + + {:else} + + {/if} +
+
+ {/if} +
+
diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 2911a06bff..64ad405bf3 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -90,25 +90,19 @@ }) const scheduleExists = await ScheduleService.existsSchedule({ workspace: $workspaceStore ?? '', - path: initialPath + path: flow.path }) if (scheduleExists) { const schedule = await ScheduleService.getSchedule({ workspace: $workspaceStore ?? '', - path: initialPath + path: flow.path }) - if ( - schedule.path != flow.path || - JSON.stringify(schedule.args) != JSON.stringify(args) || - schedule.schedule != cron - ) { + if (JSON.stringify(schedule.args) != JSON.stringify(args) || schedule.schedule != cron) { await ScheduleService.updateSchedule({ workspace: $workspaceStore ?? '', - path: initialPath, + path: flow.path, requestBody: { schedule: formatCron(cron), - script_path: flow.path, - is_flow: true, args } }) diff --git a/frontend/src/lib/components/Path.svelte b/frontend/src/lib/components/Path.svelte index 7e8c64ef78..2859bc541c 100644 --- a/frontend/src/lib/components/Path.svelte +++ b/frontend/src/lib/components/Path.svelte @@ -6,21 +6,20 @@ ResourceService, ScheduleService, ScriptService, - VariableService, - type Group + VariableService } from '$lib/gen' import { GroupService } from '$lib/gen' import { superadmin, userStore, workspaceStore } from '$lib/stores' import { sleep } from '$lib/utils' import { createEventDispatcher } from 'svelte' import Required from './Required.svelte' - import Popover from './Popover.svelte' import { Button, Drawer, DrawerContent } from './common' import { faEye, faPlus } from '@fortawesome/free-solid-svg-icons' import GroupEditor from './GroupEditor.svelte' import ToggleButtonGroup from './common/toggleButton/ToggleButtonGroup.svelte' import ToggleButton from './common/toggleButton/ToggleButton.svelte' import { Icon } from 'svelte-awesome' + import Tooltip from './Tooltip.svelte' type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app' let meta: Meta | undefined = undefined @@ -68,9 +67,6 @@ if (path == '' || path == 'u//') { meta = { ownerKind: 'user', name: namePlaceholder, owner: '' } - while ($userStore == undefined) { - await sleep(500) - } meta.owner = $userStore!.username let i = 1 @@ -85,10 +81,16 @@ } async function loadGroups(): Promise { - groups = await GroupService.listGroupNames({ - workspace: $workspaceStore!, - onlyMemberOf: !($userStore?.is_admin || $superadmin) - }) + let initialGroups: string[] = [] + if (initialPath?.split('/')?.[0] == 'g') { + initialGroups.push(initialPath?.split('/')?.[1]) + } + groups = initialGroups.concat( + await GroupService.listGroupNames({ + workspace: $workspaceStore!, + onlyMemberOf: !$userStore?.is_admin + }) + ) } async function validate(meta: Meta, path: string, kind: PathKind) { @@ -150,7 +152,7 @@ } $: { - if ($workspaceStore) { + if ($workspaceStore && $userStore) { loadGroups() initPath() } @@ -204,23 +206,14 @@
-
+
{#if meta != undefined} -
+
{#if meta.ownerKind === 'user'} -
diff --git a/frontend/src/routes/workspace_settings.svelte b/frontend/src/routes/workspace_settings.svelte index ad66f23ea0..9632718997 100644 --- a/frontend/src/routes/workspace_settings.svelte +++ b/frontend/src/routes/workspace_settings.svelte @@ -154,7 +154,7 @@ /> - {#if $userStore?.is_admin || $superadmin} + {#if $userStore?.is_admin}