diff --git a/backend/.sqlx/query-3da14e6c89a33871e4bd40524e2fd9222aa707ee76de6ce348337c3e403eb955.json b/backend/.sqlx/query-3da14e6c89a33871e4bd40524e2fd9222aa707ee76de6ce348337c3e403eb955.json new file mode 100644 index 0000000000..3c2cee1de4 --- /dev/null +++ b/backend/.sqlx/query-3da14e6c89a33871e4bd40524e2fd9222aa707ee76de6ce348337c3e403eb955.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM usr WHERE email = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "3da14e6c89a33871e4bd40524e2fd9222aa707ee76de6ce348337c3e403eb955" +} diff --git a/backend/.sqlx/query-62cd69e3dc0f8757db3481d078921f4722bbc93d52e05656b82cb42ba9803ef6.json b/backend/.sqlx/query-62cd69e3dc0f8757db3481d078921f4722bbc93d52e05656b82cb42ba9803ef6.json deleted file mode 100644 index 68c8af06ea..0000000000 --- a/backend/.sqlx/query-62cd69e3dc0f8757db3481d078921f4722bbc93d52e05656b82cb42ba9803ef6.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM usr WHERE email = $1 AND workspace_id = $2 RETURNING username", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "62cd69e3dc0f8757db3481d078921f4722bbc93d52e05656b82cb42ba9803ef6" -} diff --git a/backend/.sqlx/query-da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a.json b/backend/.sqlx/query-da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a.json deleted file mode 100644 index c4c166cd80..0000000000 --- a/backend/.sqlx/query-da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT username FROM usr WHERE workspace_id = $1 AND email = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a" -} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index df0f445015..97efd815c6 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -229b7acc60691f7e6577c44f782676d6871defdd \ No newline at end of file +cb38edaf31affeda48317433d10903245aa518f2 \ No newline at end of file diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index a01856b668..23b1fadd60 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -1079,11 +1079,16 @@ async fn handle_zombie_jobs mpsc::channel::(1); let (send_result_never_used, _send_result_rx_never_used) = mpsc::channel::(1); + let label = if job.permissioned_as != format!("u/{}", job.created_by) && job.permissioned_as != job.created_by { + format!("ephemeral-script-end-user-{}", job.created_by) + } else { + "ephemeral-script".to_string() + }; let token = create_token_for_owner( &db, &job.workspace_id, &job.permissioned_as, - "ephemeral-script", + &label, *SCRIPT_TOKEN_EXPIRY, &job.email, &job.id, diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index b28af9f502..51ea7e3697 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2025,6 +2025,27 @@ paths: items: type: string + /w/{workspace}/users/email_from_username/{username}: + get: + summary: get email from username + operationId: emailFromUsername + tags: + - user + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: username + in: path + required: true + schema: + type: string + responses: + "200": + description: email + content: + text/plain: + schema: + type: string + /users/tokens/create: post: summary: create token diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index e6844f4ae8..34bde7be47 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -585,7 +585,7 @@ async fn create_app( audit_log( &mut tx, - &authed.username, + &authed, "apps.create", ActionKind::Create, &w_id, @@ -701,7 +701,7 @@ async fn delete_app( audit_log( &mut *tx, - &authed.username, + &authed, "apps.delete", ActionKind::Delete, &w_id, @@ -873,7 +873,7 @@ async fn update_app( audit_log( &mut tx, - &authed.username, + &authed, "apps.update", ActionKind::Update, &w_id, diff --git a/backend/windmill-api/src/configs.rs b/backend/windmill-api/src/configs.rs index 1e86f34890..adeb7e9fbe 100644 --- a/backend/windmill-api/src/configs.rs +++ b/backend/windmill-api/src/configs.rs @@ -122,7 +122,7 @@ async fn update_config( audit_log( &mut *tx, - &authed.username, + &authed, "worker_config.update", ActionKind::Update, "global", @@ -149,7 +149,7 @@ async fn delete_config( audit_log( &mut *tx, - &authed.username, + &authed, "worker_config.delete", ActionKind::Delete, "global", diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index a854be7d37..45ac404165 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -15,6 +15,7 @@ use sqlx::{ pool::PoolConnection, PgConnection, Pool, Postgres, }; +use windmill_audit::audit_ee::{AuditAuthor, AuditAuthorable}; use windmill_common::{ db::{Authable, Authed}, error::Error, @@ -246,12 +247,34 @@ impl From for Authed { } } +impl From<&ApiAuthed> for AuditAuthor { + fn from(value: &ApiAuthed) -> Self { + Self { + email: value.email.clone(), + username: value.username.clone(), + username_override: value.username_override.clone(), + } + } +} + impl ApiAuthed { pub fn display_username(&self) -> &str { self.username_override.as_ref().unwrap_or(&self.username) } } +impl AuditAuthorable for ApiAuthed { + fn username(&self) -> &str { + self.username.as_str() + } + fn email(&self) -> &str { + self.email.as_str() + } + fn username_override(&self) -> Option<&str> { + self.username_override.as_deref() + } +} + impl Authable for ApiAuthed { fn is_admin(&self) -> bool { self.is_admin diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index fb23cf87ee..74b199a485 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -348,7 +348,7 @@ async fn create_flow( audit_log( &mut tx, - &authed.username, + &authed, "flows.create", ActionKind::Create, &w_id, @@ -553,7 +553,7 @@ async fn update_flow( audit_log( &mut tx, - &authed.username, + &authed, "flows.update", ActionKind::Create, &w_id, @@ -746,7 +746,7 @@ async fn archive_flow_by_path( audit_log( &mut *tx, - &authed.username, + &authed, "flows.archive", ActionKind::Delete, &w_id, @@ -813,7 +813,7 @@ async fn delete_flow_by_path( audit_log( &mut *tx, - &authed.username, + &authed, "flows.delete", ActionKind::Delete, &w_id, diff --git a/backend/windmill-api/src/folders.rs b/backend/windmill-api/src/folders.rs index ac68c1318f..fc671b0c88 100644 --- a/backend/windmill-api/src/folders.rs +++ b/backend/windmill-api/src/folders.rs @@ -223,7 +223,7 @@ async fn create_folder( audit_log( &mut *tx, - &authed.username, + &authed, "folder.create", ActionKind::Create, &w_id, @@ -356,7 +356,7 @@ async fn update_folder( audit_log( &mut *tx, - &authed.username, + &authed, "folder.update", ActionKind::Update, &w_id, @@ -522,7 +522,7 @@ async fn delete_folder( audit_log( &mut *tx, - &authed.username, + &authed, "folder.delete", ActionKind::Delete, &w_id, @@ -585,7 +585,7 @@ async fn add_owner( audit_log( &mut *tx, - &authed.username, + &authed, "folder.add_owner", ActionKind::Update, &w_id, @@ -664,7 +664,7 @@ async fn remove_owner( audit_log( &mut *tx, - &authed.username, + &authed, "folder.remove_owner", ActionKind::Update, &w_id, diff --git a/backend/windmill-api/src/groups.rs b/backend/windmill-api/src/groups.rs index e7d8063ea7..3d36cb34c5 100644 --- a/backend/windmill-api/src/groups.rs +++ b/backend/windmill-api/src/groups.rs @@ -252,7 +252,7 @@ async fn create_group( audit_log( &mut *tx, - &authed.username, + &authed, "group.create", ActionKind::Create, &w_id, @@ -296,7 +296,7 @@ async fn create_igroup( audit_log( &mut *tx, - &authed.username, + &authed, "igroup.create", ActionKind::Create, "global", @@ -337,7 +337,7 @@ async fn update_igroup( audit_log( &mut *tx, - &authed.username, + &authed, "igroup.updated", ActionKind::Delete, "global", @@ -367,7 +367,7 @@ async fn delete_igroup( audit_log( &mut *tx, - &authed.username, + &authed, "igroup.delete", ActionKind::Delete, "global", @@ -472,7 +472,7 @@ async fn delete_group( .await?; audit_log( &mut *tx, - &authed.username, + &authed, "group.delete", ActionKind::Delete, &w_id, @@ -522,7 +522,7 @@ async fn update_group( audit_log( &mut *tx, - &authed.username, + &authed, "group.edit", ActionKind::Update, &w_id, @@ -573,7 +573,7 @@ async fn add_user( audit_log( &mut *tx, - &authed.username, + &authed, "group.adduser", ActionKind::Update, &w_id, @@ -624,7 +624,7 @@ async fn add_user_igroup( audit_log( &mut *tx, - &authed.username, + &authed, "igroup.adduser", ActionKind::Update, "global", @@ -693,7 +693,7 @@ async fn remove_user_igroup( audit_log( &mut *tx, - &authed.username, + &authed, "igroup.removeuser", ActionKind::Update, "global", @@ -733,7 +733,7 @@ async fn remove_user( audit_log( &mut *tx, - &authed.username, + &authed, "group.removeuser", ActionKind::Update, &w_id, diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 897dea8b1b..c737809e66 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -51,7 +51,7 @@ use sqlx::types::JsonRawValue; use sqlx::{types::Uuid, FromRow, Postgres, Transaction}; use tower_http::cors::{Any, CorsLayer}; use urlencoding::encode; -use windmill_audit::audit_ee::audit_log; +use windmill_audit::audit_ee::{audit_log, AuditAuthor}; use windmill_audit::ActionKind; use windmill_common::worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, SERVER_CONFIG}; use windmill_common::{ @@ -317,18 +317,31 @@ async fn cancel_job_api( ) -> error::Result { let tx = db.begin().await?; - let username = match opt_authed { - Some(authed) => authed.username, - None => "anonymous".to_string(), + let audit_author = match opt_authed { + Some(authed) => (&authed).into(), + None => AuditAuthor { + email: "anonymous".to_string(), + username: "anonymous".to_string(), + username_override: None, + }, }; - let (mut tx, job_option) = - windmill_queue::cancel_job(&username, reason, id, &w_id, tx, &db, rsmq, false).await?; + let (mut tx, job_option) = windmill_queue::cancel_job( + &audit_author.username, + reason, + id, + &w_id, + tx, + &db, + rsmq, + false, + ) + .await?; if let Some(id) = job_option { audit_log( &mut *tx, - &username, + &audit_author, "jobs.cancel", ActionKind::Delete, &w_id, @@ -358,13 +371,17 @@ async fn cancel_persistent_script_api( Path((w_id, script_path)): Path<(String, StripPath)>, Json(CancelJob { reason }): Json, ) -> error::Result<()> { - let username = match opt_authed { - Some(authed) => authed.username, - None => "anonymous".to_string(), + let audit_author = match opt_authed { + Some(authed) => (&authed).into(), + None => AuditAuthor { + email: "anonymous".to_string(), + username: "anonymous".to_string(), + username_override: None, + }, }; let cancelled_job_ids = windmill_queue::cancel_persistent_script_jobs( - &username, + &audit_author.username, reason, script_path.to_path(), &w_id, @@ -375,7 +392,7 @@ async fn cancel_persistent_script_api( audit_log( &db, - &username, + &audit_author, "jobs.cancel_persistent", ActionKind::Delete, &w_id, @@ -406,18 +423,31 @@ async fn force_cancel( ) -> error::Result { let tx = db.begin().await?; - let username = match opt_authed { - Some(authed) => authed.username, - None => "anonymous".to_string(), + let audit_author = match opt_authed { + Some(authed) => (&authed).into(), + None => AuditAuthor { + email: "anonymous".to_string(), + username: "anonymous".to_string(), + username_override: None, + }, }; - let (mut tx, job_option) = - windmill_queue::cancel_job(&username, reason, id, &w_id, tx, &db, rsmq, true).await?; + let (mut tx, job_option) = windmill_queue::cancel_job( + &audit_author.username, + reason, + id, + &w_id, + tx, + &db, + rsmq, + true, + ) + .await?; if let Some(id) = job_option { audit_log( &mut *tx, - &username, + &audit_author, "jobs.force_cancel", ActionKind::Delete, &w_id, @@ -933,7 +963,6 @@ impl From for ListQueueQuery { } } - pub fn filter_list_queue_query( mut sqlb: SqlBuilder, lq: &ListQueueQuery, @@ -1396,7 +1425,7 @@ async fn resume_suspended_job_internal( { approver.approver } else { - authed.map(|x| x.username) + authed.as_ref().map(|x| x.username.clone()) }; insert_resume_job( resume_id, @@ -1419,9 +1448,17 @@ async fn resume_suspended_job_internal( } else { resume_immediately_if_relevant(parent_flow_info, job_id, &mut tx).await?; } + + let audit_author = match authed { + Some(authed) => (&authed).into(), + None => { + let approver = approver.unwrap_or_else(|| "anonymous".to_string()); + AuditAuthor { email: approver.clone(), username: approver, username_override: None } + } + }; audit_log( &mut *tx, - &approver.unwrap_or_else(|| "anonymous".to_string()), + &audit_author, "jobs.approved", ActionKind::Update, &w_id, @@ -2047,76 +2084,75 @@ pub struct UnifiedJob { } const CJ_FIELDS: &[&str] = &[ - "'CompletedJob' as typ", - "id", - "workspace_id", - "parent_job", - "created_by", - "created_at", - "started_at", - "null as scheduled_for", - "null as running", - "script_hash", - "script_path", - "null as args", - "duration_ms", - "success", - "deleted", - "canceled", - "canceled_by", - "job_kind", - "schedule_path", - "permissioned_as", - "is_flow_step", - "language", - "is_skipped", - "email", - "visible_to_owner", - "null as suspend", - "mem_peak", - "tag", - "null as concurrent_limit", - "null as concurrency_time_window_s", - "priority", - "result->'wm_labels' as labels", - ]; + "'CompletedJob' as typ", + "id", + "workspace_id", + "parent_job", + "created_by", + "created_at", + "started_at", + "null as scheduled_for", + "null as running", + "script_hash", + "script_path", + "null as args", + "duration_ms", + "success", + "deleted", + "canceled", + "canceled_by", + "job_kind", + "schedule_path", + "permissioned_as", + "is_flow_step", + "language", + "is_skipped", + "email", + "visible_to_owner", + "null as suspend", + "mem_peak", + "tag", + "null as concurrent_limit", + "null as concurrency_time_window_s", + "priority", + "result->'wm_labels' as labels", +]; const QJ_FIELDS: &[&str] = &[ - "'QueuedJob' as typ", - "id", - "workspace_id", - "parent_job", - "created_by", - "created_at", - "started_at", - "scheduled_for", - "running", - "script_hash", - "script_path", - "null as args", - "null as duration_ms", - "null as success", - "false as deleted", - "canceled", - "canceled_by", - "job_kind", - "schedule_path", - "permissioned_as", - "is_flow_step", - "language", - "false as is_skipped", - "email", - "visible_to_owner", - "suspend", - "mem_peak", - "tag", - "concurrent_limit", - "concurrency_time_window_s", - "priority", - "null as labels", - ]; + "'QueuedJob' as typ", + "id", + "workspace_id", + "parent_job", + "created_by", + "created_at", + "started_at", + "scheduled_for", + "running", + "script_hash", + "script_path", + "null as args", + "null as duration_ms", + "null as success", + "false as deleted", + "canceled", + "canceled_by", + "job_kind", + "schedule_path", + "permissioned_as", + "is_flow_step", + "language", + "false as is_skipped", + "email", + "visible_to_owner", + "suspend", + "mem_peak", + "tag", + "concurrent_limit", + "concurrency_time_window_s", + "priority", + "null as labels", +]; impl UnifiedJob { - pub fn completed_job_fields() -> &'static [&'static str] { CJ_FIELDS } @@ -4318,7 +4354,7 @@ async fn delete_completed_job<'a>( audit_log( &mut *tx, - &authed.username, + &authed, "jobs.delete", ActionKind::Delete, &w_id, diff --git a/backend/windmill-api/src/openai.rs b/backend/windmill-api/src/openai.rs index 5e23bf05ad..41962791f9 100644 --- a/backend/windmill-api/src/openai.rs +++ b/backend/windmill-api/src/openai.rs @@ -317,7 +317,7 @@ async fn proxy( let mut tx = db.begin().await?; audit_log( &mut *tx, - &authed.username, + &authed, "openai.request", ActionKind::Execute, &w_id, diff --git a/backend/windmill-api/src/raw_apps.rs b/backend/windmill-api/src/raw_apps.rs index 5ec84a1c7e..35f3b9d708 100644 --- a/backend/windmill-api/src/raw_apps.rs +++ b/backend/windmill-api/src/raw_apps.rs @@ -134,9 +134,7 @@ async fn get_data( let app = not_found_if_none(app_o, "App", path)?; let res = Response::builder().header(header::CONTENT_TYPE, "text/javascript"); - Ok(res - .body(Body::from(app)) - .unwrap()) + Ok(res.body(Body::from(app)).unwrap()) } async fn create_app( @@ -181,7 +179,7 @@ async fn create_app( audit_log( &mut *tx, - &authed.username, + &authed, "apps.create", ActionKind::Create, &w_id, @@ -217,7 +215,7 @@ async fn delete_app( .await?; audit_log( &mut *tx, - &authed.username, + &authed, "apps.delete", ActionKind::Delete, &w_id, @@ -294,7 +292,7 @@ async fn update_app( let npath = app.path.clone().unwrap_or_else(|| path.to_owned()); audit_log( &mut *tx, - &authed.username, + &authed, "apps.update", ActionKind::Update, &w_id, diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 4cf69725ef..b2a1e76419 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -24,7 +24,7 @@ use serde_json::{value::RawValue, Value}; use sql_builder::{bind::Bind, quote, SqlBuilder}; use sqlx::{FromRow, Postgres, Transaction}; use uuid::Uuid; -use windmill_audit::audit_ee::audit_log; +use windmill_audit::audit_ee::{audit_log, AuditAuthor}; use windmill_audit::ActionKind; use windmill_common::{ db::UserDB, @@ -473,15 +473,20 @@ pub async fn transform_json_value<'c>( let path = y.strip_prefix("$var:").unwrap(); let tx: Transaction<'_, Postgres> = authed_transaction_or_default(authed, user_db.clone(), db).await?; + let v = crate::variables::get_value_internal( tx, db, workspace, path, - user_db + &user_db .clone() - .map(|_| authed.username.as_str()) - .unwrap_or("backend"), + .map(|_| authed.into()) + .unwrap_or(AuditAuthor { + email: "backend".to_string(), + username: "backend".to_string(), + username_override: None, + }), ) .await?; Ok(Value::String(v)) @@ -650,7 +655,7 @@ async fn create_resource( .await?; audit_log( &mut *tx, - &authed.username, + &authed, "resources.create", ActionKind::Create, &w_id, @@ -710,7 +715,7 @@ async fn delete_resource( .await?; audit_log( &mut *tx, - &authed.username, + &authed, "resources.delete", ActionKind::Delete, &w_id, @@ -796,7 +801,7 @@ async fn update_resource( audit_log( &mut *tx, - &authed.username, + &authed, "resources.update", ActionKind::Update, &w_id, @@ -857,7 +862,7 @@ async fn update_resource_value( .await?; audit_log( &mut *tx, - &authed.username, + &authed, "resources.update", ActionKind::Update, &w_id, @@ -1001,7 +1006,7 @@ async fn create_resource_type( audit_log( &mut *tx, - &authed.username, + &authed, "resource_types.create", ActionKind::Create, &w_id, @@ -1065,7 +1070,7 @@ async fn delete_resource_type( .await?; audit_log( &mut *tx, - &authed.username, + &authed, "resource_types.delete", ActionKind::Delete, &w_id, @@ -1121,7 +1126,7 @@ async fn update_resource_type( sqlx::query(&sql).execute(&mut *tx).await?; audit_log( &mut *tx, - &authed.username, + &authed, "resource_types.update", ActionKind::Update, &w_id, diff --git a/backend/windmill-api/src/schedule.rs b/backend/windmill-api/src/schedule.rs index e084530fb1..b869cc352b 100644 --- a/backend/windmill-api/src/schedule.rs +++ b/backend/windmill-api/src/schedule.rs @@ -201,7 +201,7 @@ async fn create_schedule( audit_log( &mut tx, - &authed.username, + &authed, "schedule.create", ActionKind::Create, &w_id, @@ -284,7 +284,7 @@ async fn edit_schedule( audit_log( &mut tx, - &authed.username, + &authed, "schedule.edit", ActionKind::Update, &w_id, @@ -496,7 +496,7 @@ pub async fn set_enabled( audit_log( &mut tx, - &authed.username, + &authed, "schedule.setenabled", ActionKind::Update, &w_id, @@ -544,7 +544,7 @@ pub async fn set_enabled( // audit_log( // &mut tx, -// &authed.username, +// &authed, // "schedule.setenabled", // ActionKind::Update, // &w_id, @@ -621,7 +621,7 @@ async fn delete_schedule( audit_log( &mut *tx, - &authed.username, + &authed, "schedule.delete", ActionKind::Delete, &w_id, diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index f45cf13360..9afeb8503e 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -663,7 +663,7 @@ async fn create_script_internal<'c>( if p_hashes.is_some() && !p_hashes.unwrap().is_empty() { audit_log( &mut tx, - &authed.username, + &authed, "scripts.update", ActionKind::Update, &w_id, @@ -682,7 +682,7 @@ async fn create_script_internal<'c>( } else { audit_log( &mut tx, - &authed.username, + &authed, "scripts.create", ActionKind::Create, &w_id, @@ -1163,7 +1163,7 @@ async fn archive_script_by_path( .map_err(|e| Error::InternalErr(format!("archiving script in {w_id}: {e}")))?; audit_log( &mut *tx, - &authed.username, + &authed, "scripts.archive", ActionKind::Delete, &w_id, @@ -1215,7 +1215,7 @@ async fn archive_script_by_hash( audit_log( &mut *tx, - &authed.username, + &authed, "scripts.archive", ActionKind::Delete, &w_id, @@ -1255,7 +1255,7 @@ async fn delete_script_by_hash( audit_log( &mut *tx, - &authed.username, + &authed, "scripts.delete", ActionKind::Delete, &w_id, @@ -1322,7 +1322,7 @@ async fn delete_script_by_path( audit_log( &mut *tx, - &authed.username, + &authed, "scripts.delete", ActionKind::Delete, &w_id, diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index ee3a8a66f5..83c34d0a88 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -40,7 +40,7 @@ use sqlx::FromRow; use time::OffsetDateTime; use tower_cookies::{Cookie, Cookies}; use tracing::{Instrument, Span}; -use windmill_audit::audit_ee::audit_log; +use windmill_audit::audit_ee::{audit_log, AuditAuthor}; use windmill_audit::ActionKind; use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING; use windmill_common::users::truncate_token; @@ -72,6 +72,7 @@ pub fn workspaced_service() -> Router { .route("/whois/:username", get(whois)) .route("/whoami", get(whoami)) .route("/leave", post(leave_workspace)) + .route("/email_from_username/:username", get(email_from_username)) } pub fn global_service() -> Router { @@ -118,6 +119,16 @@ pub fn make_unauthed_service() -> Router { fn username_override_from_label(label: Option) -> Option { if label.as_ref().is_some_and(|x| x.starts_with("webhook-")) { label + } else if label + .as_ref() + .is_some_and(|x| x.starts_with("ephemeral-script-end-user-")) + { + Some( + label + .unwrap() + .trim_start_matches("ephemeral-script-end-user-") + .to_string(), + ) } else { None } @@ -919,9 +930,10 @@ async fn logout( .fetch_optional(&mut *tx) .await?; if let Some(email) = email { + let email = email.unwrap_or("noemail".to_string()); audit_log( &mut *tx, - &email.unwrap_or("noemail".to_string()), + &AuditAuthor { email: email.clone(), username: email, username_override: None }, "users.logout", ActionKind::Delete, "global", @@ -1275,7 +1287,7 @@ async fn whois( // } async fn decline_invite( - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Extension(db): Extension, Json(nu): Json, ) -> Result<(StatusCode, String)> { @@ -1284,18 +1296,18 @@ async fn decline_invite( let is_admin = sqlx::query_scalar!( "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin", nu.workspace_id, - email, + authed.email, ) .fetch_optional(&mut *tx) .await?; audit_log( &mut *tx, - &email, + &authed, "users.decline_invite", ActionKind::Delete, &nu.workspace_id, - Some(&email), + Some(&authed.email), None, ) .await?; @@ -1306,11 +1318,14 @@ async fn decline_invite( StatusCode::OK, format!( "user {} declined invite to workspace {}", - &email, nu.workspace_id + &authed.email, nu.workspace_id ), )) } else { - Err(Error::NotFound(format!("invite for {email} not found"))) + Err(Error::NotFound(format!( + "invite for {} not found", + authed.email + ))) } } @@ -1319,7 +1334,7 @@ lazy_static! { } async fn accept_invite( - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Extension(webhook): Extension, Extension(db): Extension, Extension(rsmq): Extension>, @@ -1330,7 +1345,7 @@ async fn accept_invite( let r = sqlx::query!( "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator", nu.workspace_id, - email, + authed.email, ) .fetch_optional(&mut *tx) .await?; @@ -1339,7 +1354,7 @@ async fn accept_invite( let already_in_workspace = sqlx::query_scalar!( "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)", &nu.workspace_id, - &email, + &authed.email, ) .fetch_one(&mut *tx) .await? @@ -1351,14 +1366,14 @@ async fn accept_invite( StatusCode::CREATED, format!( "user {} accepted invite to workspace {}", - &email, nu.workspace_id + &authed.email, nu.workspace_id ), )); } let username; - (tx, username) = add_user_to_workspace( + (tx, username) = join_workspace( &nu.workspace_id, - &email, + &authed, nu.username, r.is_admin, r.operator, @@ -1368,29 +1383,29 @@ async fn accept_invite( audit_log( &mut *tx, - &username, + &ApiAuthed { username: username.clone(), ..authed.clone() }, "users.accept_invite", ActionKind::Create, &nu.workspace_id, - Some(&email), + Some(&authed.email), None, ) .await?; tx.commit().await?; handle_deployment_metadata( - &email, + &authed.email, &username, &db, &nu.workspace_id, - windmill_git_sync::DeployedObject::User { email: email.clone() }, - Some(format!("User '{}' accepted invite", &email)), + windmill_git_sync::DeployedObject::User { email: authed.email.clone() }, + Some(format!("User '{}' accepted invite", &authed.email)), rsmq, true, ) .await?; webhook.send_instance_event(InstanceEvent::UserJoinedWorkspace { - email: email.clone(), + email: authed.email.clone(), workspace: nu.workspace_id.clone(), username: username, }); @@ -1398,17 +1413,20 @@ async fn accept_invite( StatusCode::CREATED, format!( "user {} accepted invite to workspace {}", - &email, nu.workspace_id + &authed.email, nu.workspace_id ), )) } else { - Err(Error::NotFound(format!("invite for {email} not found"))) + Err(Error::NotFound(format!( + "invite for {} not found", + authed.email + ))) } } -async fn add_user_to_workspace<'c>( +async fn join_workspace<'c>( w_id: &str, - email: &str, + authed: &ApiAuthed, username: Option, is_admin: bool, operator: bool, @@ -1430,7 +1448,7 @@ async fn add_user_to_workspace<'c>( "username is not allowed when username creation is automated".to_string(), )); } - get_instance_username_or_create_pending(&mut tx, &email).await? + get_instance_username_or_create_pending(&mut tx, &authed.email).await? } else { let username = username.ok_or(Error::BadRequest("username is required".to_string()))?; let already_exists_username = sqlx::query_scalar!( @@ -1460,7 +1478,7 @@ async fn add_user_to_workspace<'c>( let already_exists_email = sqlx::query_scalar!( "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)", &w_id, - username, + authed.email, ) .fetch_one(&mut *tx) .await? @@ -1469,7 +1487,7 @@ async fn add_user_to_workspace<'c>( if already_exists_email { return Err(Error::BadRequest(format!( "user with email {} already exists in workspace {}", - email, w_id + authed.email, w_id ))); } @@ -1478,7 +1496,7 @@ async fn add_user_to_workspace<'c>( (workspace_id, email, username, is_admin, operator) VALUES ($1, $2, $3, $4, $5)", &w_id, - email, + authed.email, username, is_admin, operator @@ -1496,33 +1514,30 @@ async fn add_user_to_workspace<'c>( .await?; audit_log( &mut *tx, - &username, + &AuditAuthor { username: username.clone(), ..authed.into() }, "users.add_to_workspace", ActionKind::Create, &w_id, - Some(email), + Some(&authed.email), None, ) .await?; Ok((tx, username)) } -async fn leave_instance( - Extension(db): Extension, - ApiAuthed { email, username, .. }: ApiAuthed, -) -> Result { +async fn leave_instance(Extension(db): Extension, authed: ApiAuthed) -> Result { let mut tx = db.begin().await?; - sqlx::query!("DELETE FROM password WHERE email = $1", &email) + sqlx::query!("DELETE FROM password WHERE email = $1", &authed.email) .execute(&mut *tx) .await?; audit_log( &mut *tx, - &username, + &authed, "workspaces.leave", ActionKind::Delete, "global", - Some(&email), + Some(&authed.email), None, ) .await?; @@ -1553,7 +1568,7 @@ async fn get_workspace_user( } async fn update_workspace_user( - ApiAuthed { username, email, is_admin, .. }: ApiAuthed, + authed: ApiAuthed, Extension(db): Extension, Extension(rsmq): Extension>, Path((w_id, username_to_update)): Path<(String, String)>, @@ -1561,7 +1576,7 @@ async fn update_workspace_user( ) -> Result { let mut tx = db.begin().await?; - require_admin(is_admin, &username)?; + require_admin(authed.is_admin, &authed.username)?; if let Some(a) = eu.is_admin { sqlx::query_scalar!( @@ -1598,7 +1613,7 @@ async fn update_workspace_user( audit_log( &mut *tx, - &username, + &authed, "users.update", ActionKind::Update, &w_id, @@ -1618,8 +1633,8 @@ async fn update_workspace_user( tx.commit().await?; handle_deployment_metadata( - &email, - &username, + &authed.email, + &authed.username, &db, &w_id, windmill_git_sync::DeployedObject::User { email: user_email.clone() }, @@ -1633,12 +1648,12 @@ async fn update_workspace_user( } async fn update_user( - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Path(email_to_update): Path, Extension(db): Extension, Json(eu): Json, ) -> Result { - require_super_admin(&db, &email).await?; + require_super_admin(&db, &authed.email).await?; let mut tx = db.begin().await?; if let Some(sa) = eu.is_super_admin { @@ -1653,7 +1668,7 @@ async fn update_user( audit_log( &mut *tx, - &email, + &authed, "users.update", ActionKind::Update, "global", @@ -1666,11 +1681,11 @@ async fn update_user( } async fn delete_user( - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Path(email_to_delete): Path, Extension(db): Extension, ) -> Result { - require_super_admin(&db, &email).await?; + require_super_admin(&db, &authed.email).await?; let mut tx = db.begin().await?; sqlx::query!("DELETE FROM password WHERE email = $1", &email_to_delete) @@ -1702,7 +1717,7 @@ async fn delete_user( } audit_log( &mut *tx, - &email, + &authed, "users.delete", ActionKind::Delete, "global", @@ -1720,14 +1735,14 @@ lazy_static::lazy_static! { } async fn create_user( - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Extension(db): Extension, Extension(webhook): Extension, Extension(argon2): Extension>>, Extension(rsmq): Extension>, Json(mut nu): Json, ) -> Result<(StatusCode, String)> { - require_super_admin(&db, &email).await?; + require_super_admin(&db, &authed.email).await?; let mut tx = db.begin().await?; nu.email = nu.email.to_lowercase(); @@ -1787,7 +1802,7 @@ async fn create_user( audit_log( &mut *tx, - &email, + &authed, "users.add_global", ActionKind::Create, "global", @@ -1795,19 +1810,20 @@ async fn create_user( None, ) .await?; - tx = add_to_demo_if_exists(tx, &email).await?; + tx = add_to_demo_if_exists(tx, &nu.email).await?; tx.commit().await?; - invite_user_to_all_auto_invite_worspaces(&db, &nu.email, rsmq).await?; + invite_user_to_all_auto_invite_worspaces(&db, &nu.email, rsmq, &authed).await?; send_email_if_possible( "Invited to Windmill", &format!( - "You have been granted access to Windmill by {email}. + "You have been granted access to Windmill by {}. Log in and change your password: {}/user/login?email={}&password={}&rd=%2F%23user-settings You can then join or create a workspace. Happy building!", + authed.email, BASE_URL.read().await.clone(), &nu.email, &nu.password @@ -1837,14 +1853,14 @@ pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: Str } async fn delete_workspace_user( - ApiAuthed { username, email, is_admin, .. }: ApiAuthed, + authed: ApiAuthed, Extension(db): Extension, Extension(rsmq): Extension>, Path((w_id, username_to_delete)): Path<(String, String)>, ) -> Result { let mut tx = db.begin().await?; - require_admin(is_admin, &username)?; + require_admin(authed.is_admin, &authed.username)?; let email_to_delete_o = sqlx::query_scalar!( "SELECT email FROM usr where username = $1 AND workspace_id = $2", @@ -1856,17 +1872,17 @@ async fn delete_workspace_user( let email_to_delete = not_found_if_none(email_to_delete_o, "User", &username_to_delete)?; - let username = sqlx::query_scalar!( - "DELETE FROM usr WHERE email = $1 AND workspace_id = $2 RETURNING username", + sqlx::query_scalar!( + "DELETE FROM usr WHERE email = $1 AND workspace_id = $2", email_to_delete, &w_id ) - .fetch_one(&mut *tx) + .execute(&mut *tx) .await?; sqlx::query!( "DELETE FROM usr_to_group WHERE usr = $1 AND workspace_id = $2", - &username, + &username_to_delete, &w_id ) .execute(&mut *tx) @@ -1874,7 +1890,7 @@ async fn delete_workspace_user( audit_log( &mut *tx, - &username, + &authed, "users.delete", ActionKind::Delete, &w_id, @@ -1885,8 +1901,8 @@ async fn delete_workspace_user( tx.commit().await?; handle_deployment_metadata( - &email, - &username, + &authed.email, + &authed.username, &db, &w_id, windmill_git_sync::DeployedObject::User { email: email_to_delete.clone() }, @@ -1905,14 +1921,14 @@ async fn delete_workspace_user( async fn set_password( Extension(db): Extension, Extension(argon2): Extension>>, - ApiAuthed { username, email, .. }: ApiAuthed, + authed: ApiAuthed, Json(EditPassword { password }): Json, ) -> Result { let mut tx = db.begin().await?; let custom_type = sqlx::query_scalar!( "SELECT login_type::TEXT FROM password WHERE email = $1", - &email + &authed.email ) .fetch_one(&mut *tx) .await @@ -1921,31 +1937,32 @@ async fn set_password( if custom_type != "password".to_string() { return Err(Error::BadRequest(format!( - "login type for {email} is of type {custom_type}. Cannot set password." + "login type for {} is of type {custom_type}. Cannot set password.", + authed.email ))); } sqlx::query!( "UPDATE password SET password_hash = $1 WHERE email = $2", &hash_password(argon2, password)?, - &email, + &authed.email, ) .execute(&mut *tx) .await?; audit_log( &mut *tx, - &username, + &authed, "users.setpassword", ActionKind::Update, "global", - Some(&email), + Some(&authed.email), None, ) .await?; tx.commit().await?; - Ok(format!("password of {} updated", email)) + Ok(format!("password of {} updated", authed.email)) } pub fn hash_password(argon2: Arc, password: String) -> Result { @@ -2141,7 +2158,11 @@ async fn login( audit_log( &mut *tx, - &email, + &AuditAuthor { + username: email.clone(), + email: email.clone(), + username_override: None, + }, "users.login", ActionKind::Create, "global", @@ -2215,23 +2236,25 @@ pub async fn create_session_token<'c>( async fn create_token( Extension(db): Extension, - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Json(new_token): Json, ) -> Result<(StatusCode, String)> { let token = rd_string(30); let mut tx = db.begin().await?; - let is_super_admin = - sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email) - .fetch_optional(&mut *tx) - .await? - .unwrap_or(false); + let is_super_admin = sqlx::query_scalar!( + "SELECT super_admin FROM password WHERE email = $1", + authed.email + ) + .fetch_optional(&mut *tx) + .await? + .unwrap_or(false); sqlx::query!( "INSERT INTO token (token, email, label, expiration, super_admin, scopes) VALUES ($1, $2, $3, $4, $5, $6)", token, - email, + authed.email, new_token.label, new_token.expiration, is_super_admin, @@ -2242,14 +2265,14 @@ async fn create_token( audit_log( &mut *tx, - &email, + &authed, "users.token.create", ActionKind::Create, &"global", Some(&token[0..10]), None, ) - .instrument(tracing::info_span!("token", email = &email)) + .instrument(tracing::info_span!("token", email = &authed.email)) .await?; tx.commit().await?; Ok((StatusCode::CREATED, token)) @@ -2257,11 +2280,11 @@ async fn create_token( async fn impersonate( Extension(db): Extension, - ApiAuthed { email, username, .. }: ApiAuthed, + authed: ApiAuthed, Json(new_token): Json, ) -> Result<(StatusCode, String)> { let token = rd_string(30); - require_super_admin(&db, &email).await?; + require_super_admin(&db, &authed.email).await?; if new_token.impersonate_email.is_none() { return Err(Error::BadRequest( @@ -2295,7 +2318,7 @@ async fn impersonate( audit_log( &mut *tx, - &username, + &authed, "users.impersonate", ActionKind::Delete, &"global", @@ -2344,7 +2367,7 @@ async fn list_tokens( async fn delete_token( Extension(db): Extension, - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Path(token_prefix): Path, ) -> Result { let mut tx = db.begin().await?; @@ -2355,14 +2378,14 @@ async fn delete_token( AND token LIKE concat($2::text, '%') RETURNING concat(substring(token for 10), '*****')", ) - .bind(&email) + .bind(&authed.email) .bind(&token_prefix) .fetch_all(&mut *tx) .await?; audit_log( &mut *tx, - &email, + &authed, "users.token.delete", ActionKind::Delete, &"global", @@ -2383,20 +2406,20 @@ async fn delete_token( async fn leave_workspace( Extension(db): Extension, Path(w_id): Path, - ApiAuthed { username, .. }: ApiAuthed, + authed: ApiAuthed, ) -> Result { let mut tx = db.begin().await?; sqlx::query!( "DELETE FROM usr WHERE workspace_id = $1 AND username = $2", &w_id, - username + authed.username ) .execute(&mut *tx) .await?; audit_log( &mut *tx, - &username, + &authed, "users.leave_workspace", ActionKind::Delete, &w_id, @@ -2584,18 +2607,35 @@ async fn get_instance_username_info( })) } +async fn email_from_username( + Path((w_id, username)): Path<(String, String)>, + Extension(db): Extension, +) -> Result { + let email = sqlx::query_scalar!( + "SELECT email FROM usr WHERE username = $1 AND workspace_id = $2", + &username, + &w_id + ) + .fetch_optional(&db) + .await?; + + let email = not_found_if_none(email, "user", username)?; + + Ok(email) +} + #[derive(Deserialize)] struct RenameUser { new_username: String, } async fn rename_user( - ApiAuthed { email, .. }: ApiAuthed, + authed: ApiAuthed, Path(user_email): Path, Extension(db): Extension, Json(ru): Json, ) -> Result { - require_super_admin(&db, &email).await?; + require_super_admin(&db, &authed.email).await?; let mut tx = db.begin().await?; @@ -2652,7 +2692,7 @@ async fn rename_user( audit_log( &mut *tx, - &email, + &authed, "users.rename", ActionKind::Update, "global", diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 61dfa5f695..1733d8d539 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -20,7 +20,7 @@ use axum::{ }; use hyper::StatusCode; use serde_json::Value; -use windmill_audit::audit_ee::audit_log; +use windmill_audit::audit_ee::{audit_log, AuditAuthorable}; use windmill_audit::ActionKind; use windmill_common::{ db::UserDB, @@ -151,7 +151,7 @@ async fn get_variable( if decrypt_secret { audit_log( &mut *tx, - &authed.username, + &authed, "variables.decrypt_secret", ActionKind::Execute, &w_id, @@ -192,7 +192,7 @@ async fn get_value( ) -> JsonResult { let path = path.to_path(); let tx = user_db.begin(&authed).await?; - return get_value_internal(tx, &db, &w_id, &path, &authed.username) + return get_value_internal(tx, &db, &w_id, &path, &authed) .await .map(Json); } @@ -315,7 +315,7 @@ async fn create_variable( audit_log( &mut *tx, - &authed.username, + &authed, "variables.create", ActionKind::Create, &w_id, @@ -391,7 +391,7 @@ async fn delete_variable( .await?; audit_log( &mut *tx, - &authed.username, + &authed, "variables.delete", ActionKind::Delete, &w_id, @@ -547,7 +547,7 @@ async fn update_variable( audit_log( &mut *tx, - &authed.username, + &authed, "variables.update", ActionKind::Update, &w_id, @@ -603,7 +603,7 @@ pub async fn get_value_internal<'c>( db: &DB, w_id: &str, path: &str, - username: &str, + audit_author: &impl AuditAuthorable, ) -> Result { let variable_o = sqlx::query!( "SELECT value, account, (now() > account.expires_at) as is_expired, is_secret, path from variable @@ -622,7 +622,7 @@ pub async fn get_value_internal<'c>( let r = if variable.is_secret { audit_log( &mut *tx, - username, + audit_author, "variables.decrypt_secret", ActionKind::Execute, &w_id, diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 7558214adb..5c97d03adb 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -34,7 +34,7 @@ use itertools::Itertools; use regex::Regex; use uuid::Uuid; -use windmill_audit::audit_ee::audit_log; +use windmill_audit::audit_ee::{audit_log, AuditAuthor, AuditAuthorable}; use windmill_audit::ActionKind; use windmill_common::db::UserDB; use windmill_common::s3_helpers::LargeFileStorage; @@ -448,7 +448,7 @@ async fn edit_slack_command( audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_command_script", ActionKind::Update, &w_id, @@ -533,7 +533,7 @@ async fn edit_deploy_to( audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_deploy_to", ActionKind::Update, &w_id, @@ -571,7 +571,8 @@ async fn auto_add_user( w_id: &str, operator: &bool, tx: &mut Transaction<'_, Postgres>, -) -> Result<()> { + authorable: &impl AuditAuthorable, +) -> Result { let automate_username_creation = sqlx::query_scalar!( "SELECT value FROM global_settings WHERE name = $1", AUTOMATE_USERNAME_CREATION_SETTING, @@ -645,9 +646,24 @@ async fn auto_add_user( ) .execute(&mut **tx) .await?; + let audit_author = if authorable.username() == authorable.email() && authorable.email() == email + { + // if the user is auto adding themselves (e.g. by joining the instance), we use their newly created workspace username for audit logs + AuditAuthor { + username: username.clone(), + email: email.to_string(), + username_override: None, + } + } else { + AuditAuthor { + username: authorable.username().to_string(), + email: authorable.email().to_string(), + username_override: authorable.username_override().map(|x| x.to_string()), + } + }; audit_log( &mut **tx, - &username, + &audit_author, "users.auto_invite_add", ActionKind::Create, &w_id, @@ -655,7 +671,7 @@ async fn auto_add_user( None, ) .await?; - Ok(()) + Ok(username) } async fn edit_auto_invite( @@ -720,7 +736,7 @@ async fn edit_auto_invite( .fetch_all(&mut *tx).await?); for user in users_to_auto_add.as_ref().unwrap() { - auto_add_user(&user.email, &w_id, &operator, &mut tx).await?; + auto_add_user(&user.email, &w_id, &operator, &mut tx, &authed).await?; send_email_if_possible( &format!("Added to Windmill's workspace: {w_id}"), &format!( @@ -757,7 +773,7 @@ async fn edit_auto_invite( } audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_auto_invite_domain", ActionKind::Update, &w_id, @@ -818,7 +834,7 @@ async fn edit_webhook( } audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_webhook", ActionKind::Update, &w_id, @@ -862,7 +878,7 @@ async fn edit_copilot_config( } audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_copilot_config", ActionKind::Update, &w_id, @@ -926,7 +942,7 @@ async fn edit_large_file_storage_config( let args_for_audit = format!("{:?}", new_config.large_file_storage); audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_large_file_storage_config", ActionKind::Update, &w_id, @@ -994,7 +1010,7 @@ async fn edit_git_sync_config( let args_for_audit = format!("{:?}", new_config.git_sync_settings); audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_git_sync_config", ActionKind::Update, &w_id, @@ -1058,7 +1074,7 @@ async fn edit_default_scripts( audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_default_scripts", ActionKind::Update, &w_id, @@ -1128,7 +1144,7 @@ async fn edit_default_app( let args_for_audit = format!("{:?}", new_config.default_app_path); audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_default_app", ActionKind::Update, &w_id, @@ -1221,7 +1237,7 @@ async fn edit_error_handler( } audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.edit_error_handler", ActionKind::Update, &w_id, @@ -1263,7 +1279,7 @@ async fn set_environment_variable( audit_log( &mut *tx, - &authed.username, + &authed, "workspace.set_environment_variable", ActionKind::Create, &w_id, @@ -1285,7 +1301,7 @@ async fn set_environment_variable( audit_log( &mut *tx, - &authed.username, + &authed, "workspace.delete_environment_variable", ActionKind::Delete, &w_id, @@ -1611,7 +1627,7 @@ async fn create_workspace( audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.create", ActionKind::Create, &nw.id, @@ -1643,7 +1659,7 @@ async fn edit_workspace( audit_log( &mut *tx, - &authed.username, + &authed, "workspaces.update", ActionKind::Update, &w_id, @@ -1659,9 +1675,9 @@ async fn edit_workspace( async fn archive_workspace( Extension(db): Extension, Path(w_id): Path, - ApiAuthed { is_admin, username, email, .. }: ApiAuthed, + authed: ApiAuthed, ) -> Result { - require_admin(is_admin, &username)?; + require_admin(authed.is_admin, &authed.username)?; let mut tx = db.begin().await?; sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", &w_id) .execute(&mut *tx) @@ -1669,11 +1685,11 @@ async fn archive_workspace( audit_log( &mut *tx, - &username, + &authed, "workspaces.archive", ActionKind::Update, &w_id, - Some(&email), + Some(&authed.email), None, ) .await?; @@ -1685,24 +1701,24 @@ async fn archive_workspace( async fn leave_workspace( Extension(db): Extension, Path(w_id): Path, - ApiAuthed { email, username, .. }: ApiAuthed, + authed: ApiAuthed, ) -> Result { let mut tx = db.begin().await?; sqlx::query!( "DELETE FROM usr WHERE workspace_id = $1 AND email = $2", &w_id, - &email + &authed.email ) .execute(&mut *tx) .await?; audit_log( &mut *tx, - &username, + &authed, "workspaces.leave", ActionKind::Delete, &w_id, - Some(&email), + Some(&authed.email), None, ) .await?; @@ -1714,9 +1730,9 @@ async fn leave_workspace( async fn unarchive_workspace( Extension(db): Extension, Path(w_id): Path, - ApiAuthed { is_admin, username, email, .. }: ApiAuthed, + authed: ApiAuthed, ) -> Result { - require_admin(is_admin, &username)?; + require_admin(authed.is_admin, &authed.username)?; let mut tx = db.begin().await?; sqlx::query!("UPDATE workspace SET deleted = false WHERE id = $1", &w_id) .execute(&mut *tx) @@ -1724,11 +1740,11 @@ async fn unarchive_workspace( audit_log( &mut *tx, - &username, + &authed, "workspaces.unarchive", ActionKind::Update, &w_id, - Some(&email), + Some(&authed.email), None, ) .await?; @@ -1740,7 +1756,7 @@ async fn unarchive_workspace( async fn delete_workspace( Extension(db): Extension, Path(w_id): Path, - ApiAuthed { username, email, .. }: ApiAuthed, + authed: ApiAuthed, ) -> Result { let w_id = match w_id.as_str() { "starter" => Err(Error::BadRequest( @@ -1752,7 +1768,7 @@ async fn delete_workspace( _ => Ok(w_id), }?; let mut tx = db.begin().await?; - require_super_admin(&db, &email).await?; + require_super_admin(&db, &authed.email).await?; sqlx::query!("DELETE FROM dependency_map WHERE workspace_id = $1", &w_id) .execute(&mut *tx) @@ -1859,11 +1875,11 @@ async fn delete_workspace( audit_log( &mut *tx, - &username, + &authed, "workspaces.delete", ActionKind::Delete, &w_id, - Some(&email), + Some(&authed.email), None, ) .await?; @@ -1876,6 +1892,7 @@ pub async fn invite_user_to_all_auto_invite_worspaces( db: &DB, email: &str, rsmq: Option, + authorable: &impl AuditAuthorable, ) -> Result<()> { let mut tx = db.begin().await?; let domain = email.split('@').last().unwrap(); @@ -1890,14 +1907,8 @@ pub async fn invite_user_to_all_auto_invite_worspaces( for r in workspaces { if r.auto_add.is_some() && r.auto_add.unwrap() { let operator = r.auto_invite_operator.unwrap_or(false); - auto_add_user(email, &r.workspace_id, &operator, &mut tx).await?; - let username = sqlx::query_scalar!( - "SELECT username FROM usr WHERE workspace_id = $1 AND email = $2", - r.workspace_id, - email - ) - .fetch_one(&mut *tx) - .await?; + let username = + auto_add_user(email, &r.workspace_id, &operator, &mut tx, authorable).await?; auto_added_workspace_usernames.push((r.workspace_id, username)); } else { sqlx::query!( @@ -1999,14 +2010,14 @@ If you do not have an account on {}, login with SSO or ask an admin to create an } async fn add_user( - ApiAuthed { username, email, is_admin, .. }: ApiAuthed, + authed: ApiAuthed, Extension(db): Extension, Extension(webhook): Extension, Extension(rsmq): Extension>, Path(w_id): Path, Json(mut nu): Json, ) -> Result<(StatusCode, String)> { - require_admin(is_admin, &username)?; + require_admin(authed.is_admin, &authed.username)?; nu.email = nu.email.to_lowercase(); let mut tx = db.begin().await?; @@ -2023,7 +2034,7 @@ async fn add_user( if already_exists_email { return Err(Error::BadRequest(format!( "user with email {} already exists in workspace {}", - email, w_id + nu.email, w_id ))); } @@ -2091,11 +2102,11 @@ async fn add_user( audit_log( &mut *tx, - &username, + &authed, "users.add_to_workspace", ActionKind::Create, &w_id, - Some(&email), + Some(&nu.email), None, ) .await?; @@ -2103,8 +2114,8 @@ async fn add_user( tx.commit().await?; handle_deployment_metadata( - &email, - &username, + &authed.email, + &authed.username, &db, &w_id, windmill_git_sync::DeployedObject::User { email: nu.email.clone() }, @@ -2117,9 +2128,10 @@ async fn add_user( send_email_if_possible( &format!("Added to Windmill's workspace: {w_id}"), &format!( - "You have been granted access to Windmill's workspace {w_id} by {email} + "You have been granted access to Windmill's workspace {w_id} by {} If you do not have an account on {}, login with SSO or ask an admin to create an account for you.", + authed.email, BASE_URL.read().await.clone() ), &nu.email, @@ -2810,7 +2822,7 @@ async fn change_workspace_name( audit_log( &mut *tx, - &authed.username, + &authed, "workspace.change_workspace_name", ActionKind::Update, &w_id, @@ -3135,7 +3147,7 @@ async fn change_workspace_id( audit_log( &mut *tx, - &authed.username, + &authed, "workspace.change_workspace_id", ActionKind::Update, &rw.new_id, diff --git a/backend/windmill-audit/src/audit_ee.rs b/backend/windmill-audit/src/audit_ee.rs index 5bb52e7b3f..3520eb10ac 100644 --- a/backend/windmill-audit/src/audit_ee.rs +++ b/backend/windmill-audit/src/audit_ee.rs @@ -15,10 +15,37 @@ use windmill_common::{ use crate::{ActionKind, AuditLog, ListAuditLogQuery}; use sqlx::{Postgres, Transaction}; +#[derive(Clone)] +pub struct AuditAuthor { + pub username: String, + pub email: String, + pub username_override: Option, +} + +impl AuditAuthorable for AuditAuthor { + fn email(&self) -> &str { + &self.email + } + + fn username(&self) -> &str { + &self.username + } + + fn username_override(&self) -> Option<&str> { + self.username_override.as_deref() + } +} + +pub trait AuditAuthorable { + fn username(&self) -> &str; + fn email(&self) -> &str; + fn username_override(&self) -> Option<&str>; +} + #[tracing::instrument(level = "trace", skip_all)] pub async fn audit_log<'c, E: sqlx::Executor<'c, Database = Postgres>>( _db: E, - _username: &str, + _authorable: &impl AuditAuthorable, mut _operation: &str, _action_kind: ActionKind, _w_id: &str, diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index bcef225647..0d5f6fc720 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -40,7 +40,7 @@ use tokio::{sync::RwLock, time::sleep}; use tracing::{instrument, Instrument}; use ulid::Ulid; use uuid::Uuid; -use windmill_audit::audit_ee::audit_log; +use windmill_audit::audit_ee::{audit_log, AuditAuthor}; use windmill_audit::ActionKind; #[cfg(not(feature = "enterprise"))] use windmill_common::worker::PriorityTags; @@ -3600,9 +3600,23 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>( JobKind::DeploymentCallback => "jobs.run.deployment_callback", }; + let audit_author = if format!("u/{user}") != permissioned_as && user != permissioned_as { + AuditAuthor { + email: email.to_string(), + username: permissioned_as.trim_start_matches("u/").to_string(), + username_override: Some(user.to_string()), + } + } else { + AuditAuthor { + email: email.to_string(), + username: user.to_string(), + username_override: None, + } + }; + audit_log( &mut tx, - &user, + &audit_author, operation_name, ActionKind::Execute, workspace_id, diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 103a3d0610..2a7a6da1d4 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -127,12 +127,20 @@ pub async fn create_token_for_owner_in_bg( let owner = job.permissioned_as.clone(); let email = job.email.clone(); let job_id = job.id.clone(); + + let label = if job.permissioned_as != format!("u/{}", job.created_by) + && job.permissioned_as != job.created_by + { + format!("ephemeral-script-end-user-{}", job.created_by) + } else { + "ephemeral-script".to_string() + }; tokio::spawn(async move { let token = create_token_for_owner( &db.clone(), &w_id, &owner, - "ephemeral-script", + &label, *SCRIPT_TOKEN_EXPIRY, &email, &job_id, diff --git a/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte b/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte index 8da641beac..4ca682a1d0 100644 --- a/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte +++ b/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte @@ -92,7 +92,7 @@ {date} - {#each logsByDay as { id, timestamp, username, operation: op, action_kind, resource }} + {#each logsByDay as { id, timestamp, username, operation: op, action_kind, resource, parameters }}
-
+
{username} + {#if parameters && 'end_user' in parameters} + (end user: {parameters.end_user}) + {/if}