From 6edf9b9946d613b599cb91688c4986044caaba8d Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 7 Jan 2023 09:39:50 +0100 Subject: [PATCH] feat: add archive/unarchive/delete workspace --- ...30107075643_use_internal_url_sync.down.sql | 1 + ...0230107075643_use_internal_url_sync.up.sql | 16 ++ backend/sqlx-data.json | 192 ++++++++++++++++++ backend/windmill-api/openapi.yaml | 36 +++- backend/windmill-api/src/workspaces.rs | 124 ++++++++++- cli/workspace.ts | 2 +- .../user/(user)/workspaces/+page.svelte | 17 ++ .../(logged)/workspace_settings/+page.svelte | 50 +++-- 8 files changed, 414 insertions(+), 24 deletions(-) create mode 100644 backend/migrations/20230107075643_use_internal_url_sync.down.sql create mode 100644 backend/migrations/20230107075643_use_internal_url_sync.up.sql diff --git a/backend/migrations/20230107075643_use_internal_url_sync.down.sql b/backend/migrations/20230107075643_use_internal_url_sync.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20230107075643_use_internal_url_sync.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20230107075643_use_internal_url_sync.up.sql b/backend/migrations/20230107075643_use_internal_url_sync.up.sql new file mode 100644 index 0000000000..76132f55cf --- /dev/null +++ b/backend/migrations/20230107075643_use_internal_url_sync.up.sql @@ -0,0 +1,16 @@ +-- Add up migration script here +-- Add up migration script here +UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.57.0/main.ts"; +export async function main() { + await run( + "workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN")); + + await run("hub", "pull"); +} + +async function run(...cmd: string[]) { + console.log("Running \"" + cmd.join('' '') + "\""); + await wmill.parse(cmd); +}', summary = 'Synchronize Hub Resource types with admins workspace', +description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.' +WHERE hash = -28028598712388162 AND workspace_id = 'admins'; \ No newline at end of file diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index dd54e84820..793e407a4e 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -163,6 +163,18 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)" }, + "07b984faa14193f529ae589608289a9ffb0f3d4a0e48054393e1bb8b70b4e5e0": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM flow WHERE workspace_id = $1" + }, "097a576938eac385ddc2f16a00ddc69c3ca54f5a66923291730980eeeea1f8c1": { "describe": { "columns": [], @@ -304,6 +316,18 @@ }, "query": "SELECT EXISTS (SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2 AND path != script_path)" }, + "0dc502078eeb874262568c44078d492ac6c22b97dbf2ab0a1fd00eb3156f1745": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM script WHERE workspace_id = $1" + }, "0dd3fe3ddf9cb72760687d2ee0950afdcce2d54721bfe8dba008b15e4b581956": { "describe": { "columns": [], @@ -317,6 +341,18 @@ }, "query": "DELETE FROM account WHERE id = $1 AND workspace_id = $2" }, + "0e21323c27b282ab79da7bb3418e0730ada0f3ad9b4bc38941c8ccc39685a53d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM workspace_settings WHERE workspace_id = $1" + }, "0f6af40e79a3f44ce84bd127a8af9216c82e71acf8bf4df12b078020c90d7d9d": { "describe": { "columns": [ @@ -760,6 +796,18 @@ }, "query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) RETURNING id" }, + "1eaf8d677d520c7f2f303a731de6b6d939918e41ad0d1c748d80db3fd33cb9d3": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM app WHERE workspace_id = $1" + }, "1f040850c2a82bc09789226b167c43fd4935cfbb4951760a4d527665b70a5ac7": { "describe": { "columns": [ @@ -822,6 +870,18 @@ }, "query": "UPDATE folder SET owners = array_remove(owners, $1) WHERE name = $2 AND workspace_id = $3 RETURNING name" }, + "20597f46f2793b30f926bbd18e41f4860b7a7839879b932916ea20f2d98f43f9": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM resource WHERE workspace_id = $1" + }, "20e8a8343adc4a3a28a1c4908a13047cddafc4480cf028d3455fd17347cace73": { "describe": { "columns": [ @@ -1284,6 +1344,30 @@ }, "query": "SELECT workspace_id, name, display_name, owners, extra_perms FROM folder WHERE name = $1 AND workspace_id = $2" }, + "39f1b99319f48acb95fd4247f70baaf7a38062aa8b4f70fac10215edbbd2d41c": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM schedule WHERE workspace_id = $1" + }, + "3cd8dd59567bcd1d2b6babfbb995be97cd20a0f299a7fd8fa163dc3af388603d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM folder WHERE workspace_id = $1" + }, "3d363466d79075df3f74f946eff43ca89faefca3bcdf2c533425ca3868b0369a": { "describe": { "columns": [ @@ -2204,6 +2288,18 @@ }, "query": "INSERT INTO favorite (workspace_id, usr, path, favorite_kind) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING" }, + "67411b46ce51caf84c049dbb52d0e51c9889e73c99ff16ba12a02782bbaa53c4": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "UPDATE workspace SET deleted = false WHERE id = $1" + }, "6c63bbcb45d3f51eccaea52ec862700e1f1c2426d823abd951e1eea4fd9b85aa": { "describe": { "columns": [], @@ -2375,6 +2471,18 @@ }, "query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6::text::json, dependency_job = NULL WHERE path = $7 AND workspace_id = $8" }, + "73d3ed17fd0723ba75722f394904f6ee306b59aaf8ecfcf56484d38541343f06": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM group_ WHERE workspace_id = $1" + }, "765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": { "describe": { "columns": [ @@ -2810,6 +2918,18 @@ }, "query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') ORDER BY name" }, + "8974baa530cf20156d9931123f0e046dd626e7763b54c4d0089a33bb417906f8": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM usr_to_group WHERE workspace_id = $1" + }, "8a80333c2fbf7b50fed305882de6e4ffda985d5c648cd617add6c9e6a9c03f34": { "describe": { "columns": [], @@ -3632,6 +3752,18 @@ }, "query": "SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)" }, + "a2b78a33c8122a4657e95d0da73e1de64c096a41ce7b993b267284fae5ea53fb": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM workspace_key WHERE workspace_id = $1" + }, "a34066d4a1578a13b2e322e6936ae80a0239a79148f3edce65f51a93910a1a4b": { "describe": { "columns": [ @@ -3742,6 +3874,18 @@ }, "query": "\n SELECT usage.usage FROM usage \n WHERE is_workspace = false \n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1" }, + "a8a9a0197c51ec1614a7a5f2c24e621eadeca9a3500e9b8ab34b3b2d5037ca3c": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM variable WHERE workspace_id = $1" + }, "a98b2d68f023f46ab91167d3147416df672c2aed2ba5ab70e98a9da5fa47255a": { "describe": { "columns": [], @@ -4530,6 +4674,18 @@ }, "query": "SELECT username from usr WHERE workspace_id = $1" }, + "c6f5c08cb7c641b398e77424fb79c26d4218aea34ff9fe05beac685504333053": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM usr WHERE workspace_id = $1" + }, "c732dcb4df5877560a3e75e0032179140cbe442f0b80ce1172d80903dd8a14fa": { "describe": { "columns": [], @@ -4739,6 +4895,18 @@ }, "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.path = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]" }, + "d0bcb407ce7442511add6572b100783fb6d517a23bb9a32febf38b8c47bda656": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM workspace WHERE id = $1" + }, "d12d151995da2e8357307dd99e3f7b59192832a566d56b56097d15f713430705": { "describe": { "columns": [ @@ -5156,6 +5324,18 @@ }, "query": "SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2" }, + "e533518bd272a0643529f101b1d1dcbd6287494cc87b90487d917bc56bfcf1c1": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM workspace_invite WHERE workspace_id = $1" + }, "e587299612077e6de39b5a501aa8a736cae4a21f11853f8ae66a610b4e65f49e": { "describe": { "columns": [ @@ -5299,6 +5479,18 @@ }, "query": "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 *" }, + "e6f85cdbe681ace495fde31e67339dc58460b6914440473fe94de7a7bc292af4": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM completed_job WHERE workspace_id = $1" + }, "e838b15953b567af2a2da6a4e3a04add5ec2cacabd93171a97c5fc978a64f8aa": { "describe": { "columns": [], diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 95ed3ea7ff..db9b385630 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -704,9 +704,41 @@ paths: schema: type: string - /w/{workspace}/workspaces/delete: + /w/{workspace}/workspaces/archive: + post: + summary: archive workspace + operationId: archiveWorkspace + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /workspaces/unarchive/{workspace}: + post: + summary: unarchive workspace + operationId: unarchiveWorkspace + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /workspaces/delete/{workspace}: delete: - summary: delete workspace + summary: delete workspace (require super admin) operationId: deleteWorkspace tags: - workspace diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 65c704f3c5..e0008cd756 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -41,7 +41,7 @@ pub fn workspaced_service() -> Router { Router::new() .route("/list_pending_invites", get(list_pending_invites)) .route("/update", post(edit_workspace)) - .route("/delete", delete(delete_workspace)) + .route("/archive", post(archive_workspace)) .route("/invite_user", post(invite_user)) .route("/delete_invite", post(delete_invite)) .route("/get_settings", get(get_settings)) @@ -50,7 +50,6 @@ pub fn workspaced_service() -> Router { .route("/tarball", get(tarball_workspace)) .route("/premium_info", get(premium_info)) } - pub fn global_service() -> Router { Router::new() .route("/list_as_superadmin", get(list_workspaces_as_super_admin)) @@ -60,6 +59,8 @@ pub fn global_service() -> Router { .route("/exists", post(exists_workspace)) .route("/exists_username", post(exists_username)) .route("/allowed_domain_auto_invite", get(is_allowed_auto_domain)) + .route("/unarchive/:workspace", post(unarchive_workspace)) + .route("/delete/:workspace", delete(delete_workspace)) } #[derive(FromRow, Serialize)] @@ -538,10 +539,62 @@ async fn edit_workspace( Ok(format!("Updated workspace {}", &w_id)) } -async fn delete_workspace( +async fn archive_workspace( Extension(db): Extension, Path(w_id): Path, Authed { is_admin, username, email, .. }: Authed, +) -> Result { + require_admin(is_admin, &username)?; + let mut tx = db.begin().await?; + sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", &w_id) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &username, + "workspaces.archive", + ActionKind::Update, + &w_id, + Some(&email), + None, + ) + .await?; + tx.commit().await?; + + Ok(format!("Archived workspace {}", &w_id)) +} + +async fn unarchive_workspace( + Extension(db): Extension, + Path(w_id): Path, + Authed { is_admin, username, email, .. }: Authed, +) -> Result { + require_admin(is_admin, &username)?; + let mut tx = db.begin().await?; + sqlx::query!("UPDATE workspace SET deleted = false WHERE id = $1", &w_id) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &username, + "workspaces.unarchive", + ActionKind::Update, + &w_id, + Some(&email), + None, + ) + .await?; + tx.commit().await?; + + Ok(format!("Unarchived workspace {}", &w_id)) +} + +async fn delete_workspace( + Extension(db): Extension, + Path(w_id): Path, + Authed { username, email, .. }: Authed, ) -> Result { let w_id = match w_id.as_str() { "starter" => Err(Error::BadRequest( @@ -552,9 +605,68 @@ async fn delete_workspace( )), _ => Ok(w_id), }?; - require_admin(is_admin, &username)?; let mut tx = db.begin().await?; - sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", &w_id) + require_super_admin(&mut tx, &email).await?; + + sqlx::query!("DELETE FROM script WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + sqlx::query!("DELETE FROM flow WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + sqlx::query!("DELETE FROM app WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + sqlx::query!("DELETE FROM variable WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + sqlx::query!("DELETE FROM resource WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM schedule WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM completed_job WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM usr WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!( + "DELETE FROM workspace_invite WHERE workspace_id = $1", + &w_id + ) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM usr_to_group WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM group_ WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM folder WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM workspace_key WHERE workspace_id = $1", &w_id) + .execute(&mut tx) + .await?; + + sqlx::query!( + "DELETE FROM workspace_settings WHERE workspace_id = $1", + &w_id + ) + .execute(&mut tx) + .await?; + + sqlx::query!("DELETE FROM workspace WHERE id = $1", &w_id) .execute(&mut tx) .await?; @@ -562,7 +674,7 @@ async fn delete_workspace( &mut tx, &username, "workspaces.delete", - ActionKind::Update, + ActionKind::Delete, &w_id, Some(&email), None, diff --git a/cli/workspace.ts b/cli/workspace.ts index f4f26aaa42..73cf168a97 100644 --- a/cli/workspace.ts +++ b/cli/workspace.ts @@ -192,7 +192,7 @@ export async function add( } if (opts.create) { - setClient(token, remote.substring(0, remote.length - 1)); + setClient(token, remote.endsWith('/') ? remote.substring(0, remote.length - 1) : remote); if ( !await WorkspaceService.existsWorkspace({ diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte index 1dba750990..379236be06 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte @@ -110,7 +110,24 @@ }} >{workspace.id} - {workspace.name} as {workspace.username} + {#if workspace['deleted']} + (archived) + {/if} + {#if $superadmin && workspace['deleted']} + + {/if} {/each} {:else} diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 1ad206196f..c58f4cf6d2 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -444,21 +444,41 @@ more information.

{/if} - +
+ + + {#if $superadmin} + + {/if} +
{:else}