diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 295ba6b6ee..177b26da85 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -1118,6 +1118,29 @@ }, "query": "UPDATE flow SET value = $1 WHERE path = $2 AND workspace_id = $3" }, + "388d6fd335a3f8a405b2d465892cf21a68d4b50ace25ef88c4cdf5b347c3d5eb": { + "describe": { + "columns": [ + { + "name": "?column?", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text", + "Text", + "TextArray", + "Text" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f \n WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])\n AND value::boolean)" + }, "3911bf3bbc82d87366a5297496fd2350a252bcd69f1c7c972bf566ee7eb28b0a": { "describe": { "columns": [ @@ -3953,28 +3976,6 @@ }, "query": "select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')) AND\n deleted = false" }, - "b89fc3a68c10e6b80cb4fbe84e22e139389867fcbaa2938871fb04ab6e354d85": { - "describe": { - "columns": [ - { - "name": "exists", - "ordinal": 0, - "type_info": "Bool" - } - ], - "nullable": [ - null - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Text" - ] - } - }, - "query": "SELECT EXISTS(SELECT 1 FROM usr_to_group where usr = $1 AND group_ = $2 AND workspace_id = $3)" - }, "b9468b9e16f55db11b33d8e9793e6e3ae6c5add6ca02414140adb724120a6800": { "describe": { "columns": [], @@ -5172,6 +5173,29 @@ }, "query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name, slack_email)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3, slack_email = $4" }, + "ebc06efe51532f7f60e98f62d09a3453fc1d97d93e96ec849eac96dca05236f0": { + "describe": { + "columns": [ + { + "name": "?column?", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text", + "Text", + "TextArray", + "Text" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM folder WHERE CONCAT('u/', $1::text) = ANY(owners) AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM folder, unnest(folder.owners) as o\n WHERE o = ANY($3::text[]) AND folder.name = $2 AND folder.workspace_id = $4)" + }, "ec85a425f88044c6ed4f8fcea223c28eb9fb8c16c89a52d4c4552bd149badafa": { "describe": { "columns": [ diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0b4e8a1839..505f2c1420 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -201,6 +201,24 @@ paths: schema: type: string + + /w/{workspace}/users/is_owner/{path}: + get: + summary: is owner of path + operationId: isOwnerOfPath + tags: + - user + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: is owner + content: + application/json: + schema: + type: boolean + /users/setpassword: post: summary: set password @@ -3699,6 +3717,38 @@ paths: schema: type: string + /w/{workspace}/folders/update/{name}: + post: + summary: update folder + operationId: updateFolder + tags: + - folder + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Name" + requestBody: + description: update folder + required: true + content: + application/json: + schema: + type: object + properties: + owners: + type: array + items: + type: string + extra_perms: + additionalProperties: + type: boolean + responses: + "200": + description: folder updated + content: + text/plain: + schema: + type: string + /w/{workspace}/folders/delete/{name}: delete: summary: delete folder diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 36b5b8bc80..3163980c42 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -327,7 +327,8 @@ async fn update_app( if let Some(npath) = &ns.path { if npath != path { if !authed.is_admin { - require_owner_of_path(&w_id, &authed.username, &path, &db).await?; + require_owner_of_path(&w_id, &authed.username, &authed.groups, &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 6a525f2b7d..1ae0f5e077 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -297,7 +297,7 @@ async fn update_flow( check_schedule_conflict(&mut tx, &w_id, &nf.path).await?; if !authed.is_admin { - require_owner_of_path(&w_id, &authed.username, &flow_path, &db).await?; + require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?; } let mut schedulables = sqlx::query_as!( diff --git a/backend/windmill-api/src/folders.rs b/backend/windmill-api/src/folders.rs index 761c10180e..ebdc44995c 100644 --- a/backend/windmill-api/src/folders.rs +++ b/backend/windmill-api/src/folders.rs @@ -15,15 +15,16 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; +use itertools::Itertools; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ - error::{JsonResult, Result}, + error::{self, Error, JsonResult, Result}, users::owner_to_token_owner, utils::{not_found_if_none, paginate, Pagination}, }; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Postgres, Transaction}; +use sqlx::{query_scalar, FromRow, Postgres, Transaction}; pub fn workspaced_service() -> Router { Router::new() @@ -31,13 +32,15 @@ pub fn workspaced_service() -> Router { .route("/listnames", get(list_foldernames)) .route("/create", post(create_folder)) .route("/get/:name", get(get_folder)) + .route("/update/:name", post(update_folder)) .route("/getusage/:name", get(get_folder_usage)) .route("/delete/:name", delete(delete_folder)) .route("/addowner/:name", post(add_owner)) .route("/removeowner/:name", post(remove_owner)) + .route("/is_owner", get(is_owner)) } -#[derive(FromRow, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Clone)] pub struct Folder { pub workspace_id: String, pub name: String, @@ -54,6 +57,13 @@ pub struct NewFolder { pub extra_perms: Option, } +#[derive(Deserialize)] +pub struct UpdateFolder { + pub display_name: Option, + pub owners: Option>, + pub extra_perms: Option, +} + #[derive(Deserialize)] pub struct Owner { pub owner: String, @@ -136,14 +146,39 @@ async fn create_folder( check_name_conflict(&mut tx, &w_id, &ng.name).await?; let owner = owner_to_token_owner(&authed.username, false); + let owners = &ng.owners.unwrap_or(vec![owner.clone()]); + + if let Some(extra_perms) = ng.extra_perms.clone() { + for o in owners { + if !extra_perms + .get(&o) + .and_then(|x| x.as_bool()) + .unwrap_or(false) + { + return Err(windmill_common::error::Error::BadRequest(format!( + "Owner {} would not have permission to write to folder and that is an inconsistent state", + o + ))); + } + } + } + + let extra_perms = ng.extra_perms.unwrap_or_else(|| { + let mut map = serde_json::Map::new(); + for o in owners { + map.insert(o.clone(), serde_json::json!(true)); + } + serde_json::Value::Object(map) + }); + sqlx::query_as!( Folder, "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, $2, $3, $4, $5)", w_id, ng.name, ng.display_name.unwrap_or(ng.name.clone()), - &ng.owners.unwrap_or(vec![owner.clone()]), - ng.extra_perms.unwrap_or(serde_json::json!({owner: true})) + owners, + extra_perms, ) .execute(&mut tx) .await?; @@ -163,6 +198,111 @@ async fn create_folder( Ok(format!("Created folder {}", ng.name)) } +pub async fn is_owner( + Authed { username, is_admin, groups, .. }: Authed, + Extension(db): Extension, + Path((w_id, name)): Path<(String, String)>, +) -> JsonResult { + if is_admin { + Ok(Json(true)) + } else { + Ok(Json( + require_is_owner(&name, &username, &groups, &w_id, &db) + .await + .is_ok(), + )) + } +} + +pub async fn require_is_owner( + folder_name: &str, + username: &str, + groups: &Vec, + w_id: &str, + db: &DB, +) -> Result<()> { + let is_owner = query_scalar!( + "SELECT EXISTS(SELECT 1 FROM folder WHERE CONCAT('u/', $1::text) = ANY(owners) AND name = $2 AND workspace_id = $4) OR exists( + SELECT 1 FROM folder, unnest(folder.owners) as o + WHERE o = ANY($3::text[]) AND folder.name = $2 AND folder.workspace_id = $4)", + username, + folder_name, + groups, + w_id, + ).fetch_one(db) + .await? + .unwrap_or(false); + if !is_owner { + Err(Error::BadRequest(format!( + "{} is not an owner of {} and hence is not authorized to perform this operation", + username, folder_name + ))) + } else { + Ok(()) + } +} + +async fn update_folder( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, + Json(ng): Json, +) -> Result { + use sql_builder::prelude::*; + + let mut sqlb = SqlBuilder::update_table("folder"); + sqlb.and_where_eq("name", "?".bind(&name)); + sqlb.and_where_eq("workspace_id", "?".bind(&w_id)); + + if let Some(display_name) = ng.display_name { + sqlb.set("display_name", display_name); + } + if let Some(owners) = ng.owners { + sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(","))); + } + if let Some(extra_perms) = ng.extra_perms { + sqlb.set_str("extra_perms", extra_perms.to_string()); + } + + sqlb.returning("*"); + + let mut tx = user_db.begin(&authed).await?; + + let sql = sqlb + .sql() + .map_err(|e| error::Error::InternalErr(e.to_string()))?; + let nfolder = sqlx::query_as::<_, Folder>(&sql).fetch_one(&mut tx).await?; + + if let Some(extra_perms) = nfolder.extra_perms.as_object() { + for o in nfolder.owners { + if !extra_perms + .get(&o) + .and_then(|x| x.as_bool()) + .unwrap_or(false) + { + return Err(windmill_common::error::Error::BadRequest(format!( + "Owner {} would not have permission to write to folder and that is an invalid state", + o + ))); + } + } + } + + audit_log( + &mut tx, + &authed.username, + "folder.update", + ActionKind::Update, + &w_id, + Some(&name.to_string()), + None, + ) + .await?; + + tx.commit().await?; + Ok(format!("Updated folder {}", name)) +} + pub async fn get_folderopt<'c>( db: &mut Transaction<'c, Postgres>, w_id: &str, @@ -305,6 +445,7 @@ async fn delete_folder( async fn add_owner( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, name)): Path<(String, String)>, Json(Owner { owner }): Json, @@ -312,6 +453,9 @@ async fn add_owner( let mut tx = user_db.begin(&authed).await?; not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?; + if !authed.is_admin { + require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?; + } sqlx::query!( "UPDATE folder SET owners = array_append(owners, $1) WHERE name = $2 AND workspace_id = $3 AND NOT $1 = ANY(owners) RETURNING name", @@ -364,6 +508,7 @@ pub async fn get_folders_for_user( async fn remove_owner( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, name)): Path<(String, String)>, Json(Owner { owner }): Json, @@ -371,6 +516,9 @@ async fn remove_owner( let mut tx = user_db.begin(&authed).await?; not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?; + if !authed.is_admin { + require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?; + } sqlx::query!( "UPDATE folder SET owners = array_remove(owners, $1) WHERE name = $2 AND workspace_id = $3 RETURNING name", diff --git a/backend/windmill-api/src/granular_acls.rs b/backend/windmill-api/src/granular_acls.rs index d0a94ccd39..e5da328b86 100644 --- a/backend/windmill-api/src/granular_acls.rs +++ b/backend/windmill-api/src/granular_acls.rs @@ -6,7 +6,10 @@ * LICENSE-AGPL for a copy of the license. */ -use crate::{db::UserDB, users::Authed}; +use crate::{ + db::{UserDB, DB}, + users::{require_owner_of_path, Authed}, +}; use axum::{ extract::{Extension, Path}, routing::{get, post}, @@ -34,6 +37,7 @@ pub struct GranularAcl { async fn add_granular_acl( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(GranularAcl { owner, write }): Json, @@ -44,6 +48,15 @@ async fn add_granular_acl( .ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?; let mut tx = user_db.begin(&authed).await?; + if !authed.is_admin { + if kind == "folder" { + crate::folders::require_is_owner(&path, &authed.username, &authed.groups, &w_id, &db) + .await?; + } else if kind == "group_" { + } else { + require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?; + } + } let identifier = if kind == "group_" || kind == "folder" { "name" } else { @@ -67,11 +80,15 @@ async fn add_granular_acl( async fn remove_granular_acl( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, Json(GranularAcl { owner, write: _ }): Json, ) -> Result { let path = path.to_path(); + if !authed.is_admin { + require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?; + } let (kind, path) = path .split_once('/') .ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?; diff --git a/backend/windmill-api/src/groups.rs b/backend/windmill-api/src/groups.rs index 3d4310b180..f4603db99a 100644 --- a/backend/windmill-api/src/groups.rs +++ b/backend/windmill-api/src/groups.rs @@ -23,7 +23,7 @@ use windmill_common::{ }; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Postgres, Transaction}; +use sqlx::{query_scalar, FromRow, Postgres, Transaction}; use windmill_queue::CLOUD_HOSTED; pub fn workspaced_service() -> Router { @@ -36,6 +36,7 @@ pub fn workspaced_service() -> Router { .route("/delete/:name", delete(delete_group)) .route("/adduser/:name", post(add_user)) .route("/removeuser/:name", post(remove_user)) + .route("/is_owner", get(is_owner)) } #[derive(FromRow, Serialize, Deserialize)] @@ -137,6 +138,51 @@ async fn check_name_conflict<'c>( return Ok(()); } +pub async fn is_owner( + Authed { username, is_admin, groups, .. }: Authed, + Extension(db): Extension, + Path((w_id, name)): Path<(String, String)>, +) -> JsonResult { + if is_admin { + Ok(Json(true)) + } else { + Ok(Json( + require_is_owner(&name, &username, &groups, &w_id, &db) + .await + .is_ok(), + )) + } +} + +pub async fn require_is_owner( + group_name: &str, + username: &str, + groups: &Vec, + w_id: &str, + db: &DB, +) -> Result<()> { + let is_owner = query_scalar!( + "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists( + SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f + WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[]) + AND value::boolean)", + username, + group_name, + groups, + w_id, + ).fetch_one(db) + .await? + .unwrap_or(false); + if !is_owner { + Err(Error::BadRequest(format!( + "{} is not an owner of {} and hence is not authorized to perform this operation", + username, group_name + ))) + } else { + Ok(()) + } +} + async fn create_group( authed: Authed, Extension(user_db): Extension, @@ -230,11 +276,13 @@ async fn get_group( async fn delete_group( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, name)): Path<(String, String)>, ) -> Result { let mut tx = user_db.begin(&authed).await?; + require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?; not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?; sqlx::query!( @@ -267,12 +315,14 @@ async fn delete_group( async fn update_group( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, name)): Path<(String, String)>, Json(eg): Json, ) -> Result { let mut tx = user_db.begin(&authed).await?; + require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?; not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?; sqlx::query_as!( @@ -301,12 +351,15 @@ async fn update_group( async fn add_user( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, name)): Path<(String, String)>, Json(Username { username: user_username }): Json, ) -> Result { let mut tx = user_db.begin(&authed).await?; + require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?; + not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?; sqlx::query_as!( @@ -335,11 +388,13 @@ async fn add_user( async fn remove_user( authed: Authed, + Extension(db): Extension, Extension(user_db): Extension, Path((w_id, name)): Path<(String, String)>, Json(Username { username: user_username }): Json, ) -> Result { let mut tx = user_db.begin(&authed).await?; + require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?; not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?; if &name == "all" { diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 1a737138ce..878fb5b8de 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -374,7 +374,7 @@ async fn update_resource( check_path_conflict(&mut tx, &w_id, &npath).await?; if !authed.is_admin { - require_owner_of_path(&w_id, &authed.username, &path, &db).await?; + require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?; } sqlx::query!( "UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3", diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 2522da6676..c1f43fb85e 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -260,7 +260,8 @@ async fn create_script( if ps.path != ns.path { if !authed.is_admin { - require_owner_of_path(&w_id, &authed.username, &ps.path, &db).await?; + require_owner_of_path(&w_id, &authed.username, &authed.groups, &ps.path, &db) + .await?; } } diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index 2a655bbba8..e0e631cfd8 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -10,7 +10,7 @@ use std::{sync::Arc, time::Duration}; use crate::{ db::{UserDB, DB}, - folders::{get_folderopt, get_folders_for_user}, + folders::get_folders_for_user, utils::require_super_admin, workspaces::invite_user_to_all_auto_invite_worspaces, CookieDomain, IsSecure, @@ -35,7 +35,7 @@ use tracing::{Instrument, Span}; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ error::{self, Error, JsonResult, Result}, - utils::{not_found_if_none, rd_string, require_admin, Pagination}, + utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath}, }; use windmill_queue::CLOUD_HOSTED; @@ -52,6 +52,7 @@ pub fn workspaced_service() -> Router { .route("/exists", post(exists_username)) .route("/update/:user", post(update_workspace_user)) .route("/delete/:user", delete(delete_workspace_user)) + .route("/is_owner/:path", get(is_owner_of_path)) .route("/whois/:email", get(whois)) .route("/whoami", get(whoami)) .route("/leave", post(leave_workspace)) @@ -852,21 +853,30 @@ pub async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result< .await?; Ok(groups) } - -pub async fn is_user_member(w_id: &str, username: &str, group: &str, db: &DB) -> Result { - let is_member = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM usr_to_group where usr = $1 AND group_ = $2 AND workspace_id = $3)", - username, - group, - w_id - ) - .fetch_one(db) - .await? - .unwrap_or(false); - Ok(is_member) +pub async fn is_owner_of_path( + Authed { username, is_admin, groups, .. }: Authed, + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + if is_admin { + Ok(Json(true)) + } else { + Ok(Json( + require_owner_of_path(&w_id, &username, &groups, path, &db) + .await + .is_ok(), + )) + } } -pub async fn require_owner_of_path(w_id: &str, username: &str, path: &str, db: &DB) -> Result<()> { +pub async fn require_owner_of_path( + w_id: &str, + username: &str, + groups: &Vec, + path: &str, + db: &DB, +) -> Result<()> { let splitted = path.split("/").collect::>(); if splitted[0] == "u" { if splitted[1] == username { @@ -878,24 +888,9 @@ pub async fn require_owner_of_path(w_id: &str, username: &str, path: &str, db: & ))); } } else if splitted[0] == "g" { - if is_user_member(w_id, username, splitted[1], db).await? { - 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] - ))); - } + return crate::groups::require_is_owner(w_id, username, groups, splitted[1], db).await; } else if splitted[0] == "f" { - let folder = get_folderopt(&mut db.begin().await?, w_id, splitted[1]).await?; - if folder.is_some() && folder.unwrap().owners.contains(&username.to_string()) { - return Ok(()); - } else { - return Err(Error::BadRequest(format!( - "{} is not an admin of {} and hence is not authorized to perform this destructive operation", - username, splitted[1] - ))); - } + return crate::folders::require_is_owner(w_id, username, groups, splitted[1], db).await; } Err(Error::BadRequest(format!("not recognized owner kind"))) } diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index c4de9defeb..09dc5ba73c 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -378,7 +378,7 @@ async fn update_variable( if npath != path { check_path_conflict(&mut tx, &w_id, &npath).await?; if !authed.is_admin { - require_owner_of_path(&w_id, &authed.username, &path, &db).await?; + require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?; } sqlx::query!( "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3", diff --git a/frontend/src/lib/components/FolderEditor.svelte b/frontend/src/lib/components/FolderEditor.svelte index 14f5dd633f..99cf091850 100644 --- a/frontend/src/lib/components/FolderEditor.svelte +++ b/frontend/src/lib/components/FolderEditor.svelte @@ -65,7 +65,11 @@ async function loadFolder(): Promise { folder = await FolderService.getFolder({ workspace: $workspaceStore!, name }) can_write = - folder.owners.includes('u/' + $userStore?.username) || ($userStore?.is_admin ?? false) + $userStore != undefined && + (folder?.owners.includes('u/' + $userStore.username) || + ($userStore.is_admin ?? false) || + $userStore.pgroups.findIndex((x) => folder?.owners.includes(x)) != -1) + perms = Array.from( new Set( Object.entries(folder?.extra_perms ?? {}) diff --git a/frontend/src/lib/components/MoveDrawer.svelte b/frontend/src/lib/components/MoveDrawer.svelte index 6d45acbf64..75b0e2511d 100644 --- a/frontend/src/lib/components/MoveDrawer.svelte +++ b/frontend/src/lib/components/MoveDrawer.svelte @@ -21,10 +21,10 @@ export async function openDrawer(initialPath_l: string, kind_l: Kind) { kind = kind_l initialPath = initialPath_l + await loadOwner() drawer.openDrawer() } - $: $userStore && $workspaceStore && loadOwner() async function loadOwner() { own = await isOwner(path, $userStore!, $workspaceStore!) @@ -73,7 +73,7 @@

Move {initialPath} to

{#if !own} Since you do not own this item, you cannot move this item(you can however fork it!)Since you do not own this item, you cannot move this item (you can however fork it) {/if} diff --git a/frontend/src/lib/components/Path.svelte b/frontend/src/lib/components/Path.svelte index 41aa7f18d2..98f6585824 100644 --- a/frontend/src/lib/components/Path.svelte +++ b/frontend/src/lib/components/Path.svelte @@ -305,7 +305,7 @@ }} > User - Group + Folder diff --git a/frontend/src/lib/components/ShareModal.svelte b/frontend/src/lib/components/ShareModal.svelte index fb910ef801..9ae96f78f0 100644 --- a/frontend/src/lib/components/ShareModal.svelte +++ b/frontend/src/lib/components/ShareModal.svelte @@ -38,11 +38,10 @@ loadAcls() loadGroups() loadUsernames() + await loadOwner() drawer.openDrawer() } - $: $userStore && $workspaceStore && loadOwner() - async function loadOwner() { own = await isOwner(path, $userStore!, $workspaceStore!) } diff --git a/frontend/src/lib/components/common/table/FlowRow.svelte b/frontend/src/lib/components/common/table/FlowRow.svelte index 43126312b8..e05b580aa0 100644 --- a/frontend/src/lib/components/common/table/FlowRow.svelte +++ b/frontend/src/lib/components/common/table/FlowRow.svelte @@ -141,7 +141,8 @@ icon: faFileExport, action: () => { moveDrawer.openDrawer(path, 'flow') - } + }, + disabled: !canWrite }, { displayName: 'Schedule', diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index 2a8c465d3e..a202650a1f 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -161,7 +161,8 @@ icon: faFileExport, action: () => { moveDrawer.openDrawer(path, 'script') - } + }, + disabled: !canWrite }, { displayName: 'View runs', diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 0b6f5cd194..24121ba030 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -5,8 +5,8 @@ import { FolderService, Script, ScriptService, + UserService, type Flow, - type FlowModule, type User } from '$lib/gen' import { toast } from '@zerodevx/svelte-toast' @@ -68,13 +68,12 @@ export function displayDate(dateString: string | undefined, displaySecond = fals if (date.toString() === 'Invalid Date') { return '' } else { - return `${date.getFullYear()}/${ - date.getMonth() + 1 - }/${date.getDate()} at ${date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - second: displaySecond ? '2-digit' : undefined - })}` + return `${date.getFullYear()}/${date.getMonth() + 1 + }/${date.getDate()} at ${date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: displaySecond ? '2-digit' : undefined + })}` } } @@ -185,14 +184,12 @@ export function removeItemAll(arr: T[], value: T) { } export async function isOwner(path: string, user: UserExt, workspace: string): Promise { - if (isObviousOwner(path, user)) { + if (user.is_admin && (workspace != 'starter' || user.is_super_admin)) { return true - } else if (path.startsWith('f/')) { - let folder = path.split('/')[1] - let res = await FolderService.getFolder({ workspace, name: folder }) - return res.owners.includes('u/' + user.username) - } else { + } else if (workspace == 'starter') { return false + } else { + return await UserService.isOwnerOfPath({ path: path, workspace: workspace }) } }