From 126459f9725dca273f402f94aa5197bded81f491 Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Thu, 23 May 2024 17:23:50 +0200 Subject: [PATCH] feat: improve permissioned audit logs (#3799) * fix: increase token label size in db * Revert "Revert "feat: improve premissioned audit logs (#3793)" (#3798)" This reverts commit afae33f5f8bb20eb36db1d7060b61b7c148b7d07. * feat: add username to email function * fix: nit * fix: add docstring to global func --- ...fd9222aa707ee76de6ce348337c3e403eb955.json | 15 + ...21f4722bbc93d52e05656b82cb42ba9803ef6.json | 23 - ...a5f7360008e082e2fb8d58faff4461e18c83a.json | 23 - backend/ee-repo-ref.txt | 2 +- ...3095814_increase_token_label_size.down.sql | 1 + ...523095814_increase_token_label_size.up.sql | 2 + backend/src/monitor.rs | 7 +- backend/windmill-api/openapi.yaml | 21 + backend/windmill-api/src/apps.rs | 6 +- backend/windmill-api/src/configs.rs | 4 +- backend/windmill-api/src/db.rs | 23 + backend/windmill-api/src/flows.rs | 8 +- backend/windmill-api/src/folders.rs | 10 +- backend/windmill-api/src/groups.rs | 20 +- backend/windmill-api/src/jobs.rs | 214 +- backend/windmill-api/src/openai.rs | 2 +- backend/windmill-api/src/raw_apps.rs | 10 +- backend/windmill-api/src/resources.rs | 27 +- backend/windmill-api/src/schedule.rs | 10 +- backend/windmill-api/src/scripts.rs | 12 +- backend/windmill-api/src/variables.rs | 16 +- backend/windmill-api/src/workspaces.rs | 118 +- backend/windmill-audit/src/audit_ee.rs | 29 +- backend/windmill-queue/src/jobs.rs | 18 +- backend/windmill-worker/src/worker.rs | 10 +- .../auditLogs/AuditLogsTable.svelte | 7 +- python-client/wmill/wmill/client.py | 41 +- typescript-client/build.jsr.sh | 2 +- typescript-client/build.sh | 2 +- typescript-client/client.ts | 13 + typescript-client/package-lock.json | 4 +- typescript-client/windmill.d.ts | 16966 ---------------- typescript-client/windmill.js | 9946 --------- 33 files changed, 426 insertions(+), 27186 deletions(-) create mode 100644 backend/.sqlx/query-3da14e6c89a33871e4bd40524e2fd9222aa707ee76de6ce348337c3e403eb955.json delete mode 100644 backend/.sqlx/query-62cd69e3dc0f8757db3481d078921f4722bbc93d52e05656b82cb42ba9803ef6.json delete mode 100644 backend/.sqlx/query-da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a.json create mode 100644 backend/migrations/20240523095814_increase_token_label_size.down.sql create mode 100644 backend/migrations/20240523095814_increase_token_label_size.up.sql delete mode 100644 typescript-client/windmill.d.ts delete mode 100644 typescript-client/windmill.js 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/migrations/20240523095814_increase_token_label_size.down.sql b/backend/migrations/20240523095814_increase_token_label_size.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20240523095814_increase_token_label_size.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20240523095814_increase_token_label_size.up.sql b/backend/migrations/20240523095814_increase_token_label_size.up.sql new file mode 100644 index 0000000000..f36d0cc100 --- /dev/null +++ b/backend/migrations/20240523095814_increase_token_label_size.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE token ALTER COLUMN label TYPE VARCHAR(1000) 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..3d48b853b1 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2025,6 +2025,27 @@ paths: items: type: string + /w/{workspace}/users/username_to_email/{username}: + get: + summary: get email from username + operationId: usernameToEmail + 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/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}