mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
3dadcbe865
* Add create_ephemeral workspace endpoint * Add cli devShell * List ephemeral workspaces + improve endpoint * Add postgres function to clone a workspace (to be revisited) * Clone workspace using the postgres function * Add first iteration of ephemeral workspaces command * Update display of forked workspaces * Remove SQLX_OFFLINE * Add UI to create ephemeral workspace * Add option to exclude repository from being inherited to forks * WIP: reworking cloning logic * Fix cloning * Fix redirect after creating fork * Clean up cloning behaviour * Rename ephemeral to fork * emove ephemeral_workspaces table in favour of columns in workspaces * Fix display of forked workspaces * Fix skip inherit git sync repo setting * Fix fork invite display + creating fork as user * Fix SideMenu bug * Fix alignment * Simplify migrations * Update deletion of workspaces * Delete forked workspace from cli * Deleting fork workspaces from the UI as non-admin * Update cli sync and fork creation to adapt to branches and forks * Update fork prefix * Remove skip tracking toggle * Fix npm check warnings * Fix last npm check * fix: force stdin to Stdio::null for all user code execution (#6575) Set stdin to Stdio::null for all Commands that execute user code across all supported languages to prevent unwanted input consumption. This affects Python, Deno, Bash, PowerShell, Go, Rust, PHP, Ruby, Java, C#, Ansible, Nu, and Bun executors. The dedicated worker handler was intentionally left unchanged as it requires stdin for inter-process communication. Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> * Update ee-repo ref * Update SQLx metadata * Fix typos --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
591 lines
14 KiB
Rust
591 lines
14 KiB
Rust
use crate::db::ApiAuthed;
|
|
|
|
use crate::workspaces::{check_w_id_conflict, CREATE_WORKSPACE_REQUIRE_SUPERADMIN, WM_FORK_PREFIX};
|
|
use crate::{db::DB, utils::require_super_admin};
|
|
|
|
use axum::{
|
|
extract::{Extension, Path},
|
|
Json,
|
|
};
|
|
|
|
use sqlx::{Postgres, Transaction};
|
|
use windmill_audit::audit_oss::audit_log;
|
|
use windmill_audit::ActionKind;
|
|
|
|
use windmill_common::worker::CLOUD_HOSTED;
|
|
|
|
use windmill_common::{
|
|
auth::is_super_admin_email,
|
|
error::{Error, Result},
|
|
utils::require_admin,
|
|
};
|
|
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Deserialize)]
|
|
pub(crate) struct ChangeWorkspaceId {
|
|
new_id: String,
|
|
new_name: String,
|
|
}
|
|
|
|
pub(crate) async fn change_workspace_id(
|
|
authed: ApiAuthed,
|
|
Path(old_id): Path<String>,
|
|
Extension(db): Extension<DB>,
|
|
Json(rw): Json<ChangeWorkspaceId>,
|
|
) -> Result<String> {
|
|
if *CLOUD_HOSTED && !is_super_admin_email(&db, &authed.email).await? {
|
|
return Err(Error::BadRequest(
|
|
"This feature is not available on the cloud".to_string(),
|
|
));
|
|
}
|
|
|
|
if *CREATE_WORKSPACE_REQUIRE_SUPERADMIN {
|
|
require_super_admin(&db, &authed.email).await?;
|
|
} else {
|
|
require_admin(authed.is_admin, &authed.username)?;
|
|
}
|
|
|
|
let mut tx = db.begin().await?;
|
|
|
|
check_w_id_conflict(&mut tx, &rw.new_id).await?;
|
|
|
|
// duplicate workspace with new id name
|
|
sqlx::query!(
|
|
"INSERT INTO workspace SELECT $1, $2, owner, deleted, premium FROM workspace WHERE id = $3",
|
|
&rw.new_id,
|
|
&rw.new_name,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE account SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE app SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE audit SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE capture SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE capture_config SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE http_trigger SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE websocket_trigger SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE kafka_trigger SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE nats_trigger SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE v2_job_completed SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE dependency_map SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE deployment_metadata SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE draft SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE favorite SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"INSERT INTO flow
|
|
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)
|
|
SELECT $1, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at
|
|
FROM flow WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE flow_version SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE workspace_runnable_dependencies SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE asset SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE flow_node SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!("DELETE FROM flow WHERE workspace_id = $1", &old_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
// have to duplicate group_ with new workspace id because of foreign key constraint
|
|
sqlx::query!(
|
|
"INSERT INTO group_ SELECT $1, name, summary, extra_perms FROM group_ WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE usr_to_group SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
// then delete old group_
|
|
sqlx::query!("DELETE FROM group_ WHERE workspace_id = $1", &old_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE folder SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE input SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE job_logs SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE job_stats SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE v2_job_queue SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE v2_job SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE raw_app SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE resource SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE resource_type SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE schedule SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE script SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE token SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE usage SET id = $1 WHERE id = $2 AND is_workspace = true",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE usr SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE variable SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE workspace_env SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE workspace_invite SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE workspace_key SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"UPDATE workspace_settings SET workspace_id = $1 WHERE workspace_id = $2",
|
|
&rw.new_id,
|
|
&old_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
// delete old workspace
|
|
sqlx::query!("DELETE FROM workspace WHERE id = $1", &old_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
audit_log(
|
|
&mut *tx,
|
|
&authed,
|
|
"workspace.change_workspace_id",
|
|
ActionKind::Update,
|
|
&rw.new_id,
|
|
Some(&authed.email),
|
|
None,
|
|
)
|
|
.await?;
|
|
tx.commit().await?;
|
|
Ok(format!(
|
|
"updated workspace from {} to {}",
|
|
&old_id, &rw.new_id
|
|
))
|
|
}
|
|
|
|
pub(crate) async fn delete_workspace(
|
|
Extension(db): Extension<DB>,
|
|
Path(w_id): Path<String>,
|
|
authed: ApiAuthed,
|
|
) -> Result<String> {
|
|
let w_id = match w_id.as_str() {
|
|
"starter" => Err(Error::BadRequest(
|
|
"starter workspace cannot be deleted".to_string(),
|
|
)),
|
|
"admins" => Err(Error::BadRequest(
|
|
"admins workspace cannot be deleted".to_string(),
|
|
)),
|
|
_ => Ok(w_id),
|
|
}?;
|
|
let mut tx = db.begin().await?;
|
|
if !(w_id.starts_with(WM_FORK_PREFIX) && is_workspace_owner(&authed, &w_id, &mut tx).await?) {
|
|
require_super_admin(&db, &authed.email).await?;
|
|
}
|
|
|
|
sqlx::query!("DELETE FROM dependency_map WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
sqlx::query!("DELETE FROM v2_job_queue WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
sqlx::query!("DELETE FROM v2_job WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
sqlx::query!("DELETE FROM capture WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
// capture_config has on delete cascade
|
|
|
|
sqlx::query!("DELETE FROM draft WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.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 raw_app WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
sqlx::query!("DELETE FROM input 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 v2_job_completed WHERE workspace_id = $1",
|
|
&w_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!("DELETE FROM job_stats WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"DELETE FROM deployment_metadata 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 resource_type 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 account 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 token WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!("DELETE FROM http_trigger WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!(
|
|
"DELETE FROM websocket_trigger WHERE workspace_id = $1",
|
|
&w_id
|
|
)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query!("DELETE FROM kafka_trigger WHERE workspace_id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
// NATS triggers have on delete cascade
|
|
|
|
sqlx::query!("DELETE FROM workspace WHERE id = $1", &w_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
audit_log(
|
|
&mut *tx,
|
|
&authed,
|
|
"workspaces.delete",
|
|
ActionKind::Delete,
|
|
&w_id,
|
|
Some(&authed.email),
|
|
None,
|
|
)
|
|
.await?;
|
|
tx.commit().await?;
|
|
|
|
Ok(format!("Deleted workspace {}", &w_id))
|
|
}
|
|
|
|
async fn is_workspace_owner(
|
|
authed: &ApiAuthed,
|
|
w_id: &str,
|
|
tx: &mut Transaction<'_, Postgres>,
|
|
) -> Result<bool> {
|
|
let owner = sqlx::query_scalar!("SELECT owner FROM workspace WHERE id = $1", w_id)
|
|
.fetch_optional(&mut **tx)
|
|
.await?;
|
|
Ok(owner.map(|o| o == authed.email).unwrap_or(false))
|
|
}
|