From 1f03eb96deeff42077e6e1d7664c490cb7c949e6 Mon Sep 17 00:00:00 2001 From: wendrul <53628737+wendrul@users.noreply.github.com> Date: Fri, 4 Jul 2025 06:02:45 +0900 Subject: [PATCH] feat: Better tracing for audit logs, including a graph to visualize them (#6078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Migrate audit log page to svelte 5 * Add email and span cols to audit table * Add token_prefixs to audit logs (into AuditAuthorable trait) * Add audit logs graph (wip) * Add audit span on push and jwt * Unify same job audit into the same audit span * Improve the graph visually * Fix typo * functioning graph with svelte issue * Fix leak * feat: migrate AuditLogsTable from DataTable to VirtualList for performance - Replace DataTable component with VirtualList for handling thousands of rows - Migrate to Svelte 5 runes ($props, $bindable, $derived, $state) - Implement flattenLogs() for virtual scrolling with grouped date headers - Add sticky indices and dynamic height calculation - Update parent component to use callback prop pattern instead of events - Preserve all existing functionality: filtering, selection, pagination - Follows RunsTable.svelte implementation pattern Resolves performance issues when displaying large audit log datasets. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Ruben Fiszel * Fix remaining virtual list issues * WIP graph * Fix chart styling * Fix npm check * Fix missing audit_span arguments * Update sqlx * use varchar 255 for email as in other tables * Remove syntax inconsistency * Match struct with ee crate * Update ee-repo-ref.txt * Update worker_flow.rs * Remove redefinition of trait to prevent shadowing * Re add trait on oss but only when no `private` flag --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel Co-authored-by: GitHub Action Co-authored-by: Ruben Fiszel --- backend/ee-repo-ref.txt | 2 +- ...50135_add_email_and_span_to_audit.down.sql | 3 + ...5150135_add_email_and_span_to_audit.up.sql | 3 + backend/src/monitor.rs | 1 + backend/tests/worker.rs | 6 +- backend/windmill-api/openapi.yaml | 2 + backend/windmill-api/src/approvals.rs | 4 + backend/windmill-api/src/apps.rs | 9 + backend/windmill-api/src/auth.rs | 10 +- backend/windmill-api/src/db.rs | 6 + backend/windmill-api/src/flows.rs | 2 + backend/windmill-api/src/jobs.rs | 83 ++- backend/windmill-api/src/resources.rs | 1 + backend/windmill-api/src/schedule.rs | 11 +- backend/windmill-api/src/scripts.rs | 1 + backend/windmill-api/src/slack_approvals.rs | 12 +- backend/windmill-api/src/users.rs | 34 +- backend/windmill-api/src/variables.rs | 9 +- backend/windmill-audit/src/audit_oss.rs | 24 +- backend/windmill-audit/src/lib.rs | 1 + backend/windmill-common/src/auth.rs | 15 +- backend/windmill-common/src/db.rs | 1 + backend/windmill-queue/src/jobs.rs | 11 +- backend/windmill-queue/src/schedule.rs | 1 + .../windmill-worker/src/result_processor.rs | 7 +- backend/windmill-worker/src/worker_flow.rs | 3 + .../windmill-worker/src/worker_lockfiles.rs | 1 + .../auditLogs/AuditLogsFilters.svelte | 14 +- .../auditLogs/AuditLogsTable.svelte | 379 ++++++++---- .../auditLogs/AuditLogsTimeline.svelte | 564 ++++++++++++++++++ .../(root)/(logged)/audit_logs/+page.svelte | 205 ++++--- 31 files changed, 1165 insertions(+), 260 deletions(-) create mode 100644 backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql create mode 100644 backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql create mode 100644 frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 188513dd18..4645cca3de 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -b7c6fc065a3da98933d50fae697784da61c3fe2d +21aabec96e91c8075dd637d1e32af90e495082fc diff --git a/backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql b/backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql new file mode 100644 index 0000000000..637b360023 --- /dev/null +++ b/backend/migrations/20250605150135_add_email_and_span_to_audit.down.sql @@ -0,0 +1,3 @@ +-- Remove email and span columns from audit table +ALTER TABLE audit DROP COLUMN email; +ALTER TABLE audit DROP COLUMN span; diff --git a/backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql b/backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql new file mode 100644 index 0000000000..b75b49af9f --- /dev/null +++ b/backend/migrations/20250605150135_add_email_and_span_to_audit.up.sql @@ -0,0 +1,3 @@ +-- Add email and span columns to audit table +ALTER TABLE audit ADD COLUMN email VARCHAR(255); +ALTER TABLE audit ADD COLUMN span VARCHAR(255); diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 4b4946e3e4..ff07a31bd0 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -1926,6 +1926,7 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, worker &job.email, &job.id, None, + Some(format!("handle_zombie_jobs")), ) .await .expect("could not create job token"); diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 184bb9daa6..9403df9f08 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -322,7 +322,7 @@ mod suspend_resume { let second = completed.next().await.unwrap(); // print_job(second, &db).await; - let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap(); + let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap(); let secret = reqwest::get(format!( "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben" )) @@ -427,7 +427,7 @@ mod suspend_resume { /* ... and send a request resume it. */ let second = completed.next().await.unwrap(); - let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None).await.unwrap(); + let token = windmill_common::auth::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "", &Uuid::nil(), None, None).await.unwrap(); let secret = reqwest::get(format!( "http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}" )) @@ -935,6 +935,7 @@ impl RunJob { /* user */ "test-user", /* email */ "test@windmill.dev", /* permissioned_as */ "u/test-user".to_string(), + /* token_prefix */ None, /* scheduled_for_o */ None, /* schedule_path */ None, /* parent_job */ None, @@ -4209,6 +4210,7 @@ async fn test_result_format(db: Pool) { "", &Uuid::nil(), None, + None, ) .await .unwrap(); diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0e3aa9ab3f..7633f7b3e7 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -14304,6 +14304,8 @@ components: type: string parameters: type: object + span: + type: string required: - id - timestamp diff --git a/backend/windmill-api/src/approvals.rs b/backend/windmill-api/src/approvals.rs index f4ee9dd843..f16ed00abe 100644 --- a/backend/windmill-api/src/approvals.rs +++ b/backend/windmill-api/src/approvals.rs @@ -4,6 +4,7 @@ use uuid::Uuid; use std::str::FromStr; use regex::Regex; use serde_json::Value; +use crate::auth::OptTokened; use crate::db::{ApiAuthed, DB}; use crate::jobs::{cancel_suspended_job, resume_suspended_job, QueryApprover, QueryOrBody, ResumeUrls, get_resume_urls_internal}; use axum::{extract::{Path, Query}, Extension}; @@ -101,6 +102,7 @@ pub fn extract_w_id_from_resume_url(resume_url: &str) -> Result<&str, Error> { pub async fn handle_resume_action( authed: Option, + opt_tokened: OptTokened, db: DB, resume_url: &str, form_data: Value, @@ -135,6 +137,7 @@ pub async fn handle_resume_action( let res = if action == "resume" { resume_suspended_job( authed, + opt_tokened, Extension(db.clone()), Path(( w_id.to_string(), @@ -149,6 +152,7 @@ pub async fn handle_resume_action( } else { cancel_suspended_job( authed, + opt_tokened, Extension(db.clone()), Path(( w_id.to_string(), diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 85d1745caa..ad4020a671 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -9,6 +9,7 @@ use std::{collections::HashMap, sync::Arc}; */ use crate::{ + auth::OptTokened, db::{ApiAuthed, DB}, resources::get_resource_value_interpolated_internal, users::{require_owner_of_path, OptAuthed}, @@ -52,6 +53,7 @@ use windmill_audit::audit_oss::audit_log; use windmill_audit::ActionKind; use windmill_common::{ apps::{AppScriptId, ListAppQuery}, + auth::TOKEN_PREFIX_LEN, cache::{self, future::FutureCachedExt}, db::UserDB, error::{to_anyhow, Error, JsonResult, Result}, @@ -1040,6 +1042,7 @@ async fn create_app_internal<'a>( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -1411,6 +1414,7 @@ async fn update_app_internal<'a>( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -1517,6 +1521,7 @@ fn empty_triggerables(mut policy: Policy) -> Policy { async fn execute_component( OptAuthed(opt_authed): OptAuthed, + tokened: OptTokened, Extension(db): Extension, Extension(user_db): Extension, Path((w_id, path)): Path<(String, StripPath)>, @@ -1719,6 +1724,10 @@ async fn execute_component( &username, email, permissioned_as, + opt_authed + .and_then(|a| a.token_prefix) + .or_else(|| tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string())) + .as_deref(), None, None, None, diff --git a/backend/windmill-api/src/auth.rs b/backend/windmill-api/src/auth.rs index 954fcad43c..a61e2ee72b 100644 --- a/backend/windmill-api/src/auth.rs +++ b/backend/windmill-api/src/auth.rs @@ -21,7 +21,7 @@ use std::sync::{ use tokio::sync::RwLock; use windmill_common::{ - auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims}, + auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, TOKEN_PREFIX_LEN}, jwt, users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL}, }; @@ -134,6 +134,7 @@ impl AuthCache { folders: claims.folders, scopes: None, username_override, + token_prefix: claims.audit_span, }; AUTH_CACHE.insert( @@ -217,6 +218,7 @@ impl AuthCache { folders, scopes: None, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } else { let groups = vec![name.to_string()]; @@ -238,6 +240,7 @@ impl AuthCache { folders, scopes: None, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } } else { @@ -252,6 +255,7 @@ impl AuthCache { folders, scopes: None, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } } @@ -299,6 +303,7 @@ impl AuthCache { folders, scopes, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } None if super_admin => Some(ApiAuthed { @@ -310,6 +315,7 @@ impl AuthCache { folders: vec![], scopes, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }), None => None, } @@ -323,6 +329,7 @@ impl AuthCache { folders: Vec::new(), scopes, username_override, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } } @@ -355,6 +362,7 @@ impl AuthCache { folders: Vec::new(), scopes: None, username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }) } else { None diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 6e3343494d..fc21edc5de 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -836,6 +836,7 @@ pub struct ApiAuthed { pub folders: Vec<(String, bool, bool)>, pub scopes: Option>, pub username_override: Option, + pub token_prefix: Option, } impl From for Authed { @@ -848,6 +849,7 @@ impl From for Authed { groups: value.groups, folders: value.folders, scopes: value.scopes, + token_prefix: value.token_prefix, } } } @@ -858,6 +860,7 @@ impl From<&ApiAuthed> for AuditAuthor { email: value.email.clone(), username: value.username.clone(), username_override: value.username_override.clone(), + token_prefix: value.token_prefix.clone(), } } } @@ -878,6 +881,9 @@ impl AuditAuthorable for ApiAuthed { fn username_override(&self) -> Option<&str> { self.username_override.as_deref() } + fn token_prefix(&self) -> Option<&str> { + self.token_prefix.as_deref() + } } impl Authable for ApiAuthed { diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index af0c8cef7a..40f87a1f94 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -494,6 +494,7 @@ async fn create_flow( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -935,6 +936,7 @@ async fn update_flow( &authed.username, &authed.email, windmill_common::users::username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 862c472933..f0421bc031 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -22,7 +22,7 @@ use std::ops::{Deref, DerefMut}; use std::str::FromStr; use tokio::io::AsyncReadExt; use tower::ServiceBuilder; -use windmill_common::auth::is_super_admin_email; +use windmill_common::auth::{is_super_admin_email, TOKEN_PREFIX_LEN}; use windmill_common::error::JsonResult; use windmill_common::flow_status::{JobResult, RestartedFrom}; use windmill_common::jobs::{format_completed_job_result, format_result, ENTRYPOINT_OVERRIDE}; @@ -33,6 +33,7 @@ use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH; use windmill_common::variables::get_workspace_key; use crate::add_webhook_allowed_origin; +use crate::auth::{OptTokened, Tokened}; use crate::concurrency_groups::join_concurrency_key; use crate::db::ApiAuthed; @@ -293,6 +294,7 @@ struct JsonPath { } async fn get_result_by_id( authed: ApiAuthed, + tokened: Tokened, Extension(db): Extension, Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>, Query(JsonPath { json_path, .. }): Query, @@ -301,7 +303,7 @@ async fn get_result_by_id( windmill_queue::get_result_by_id(db.clone(), w_id.clone(), flow_id, node_id, json_path) .await?; - log_job_view(&db, Some(&authed), &w_id, &flow_id).await?; + log_job_view(&db, Some(&authed), Some(&tokened.token), &w_id, &flow_id).await?; Ok(Json(res)) } @@ -337,6 +339,7 @@ async fn get_db_clock(Extension(db): Extension) -> windmill_common::error::J async fn cancel_job_api( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Json(CancelJob { reason }): Json, @@ -349,6 +352,7 @@ async fn cancel_job_api( username: "anonymous".to_string(), username_override: None, email: "anonymous".to_string(), + token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()), }, }; let (mut tx, job_option) = tokio::time::timeout( @@ -447,6 +451,7 @@ async fn cancel_persistent_script_api( async fn force_cancel( OptAuthed(opt_authed): OptAuthed, + tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Json(CancelJob { reason }): Json, @@ -459,6 +464,7 @@ async fn force_cancel( username: "anonymous".to_string(), username_override: None, email: "anonymous".to_string(), + token_prefix: tokened.token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()), }, }; @@ -510,6 +516,7 @@ async fn force_cancel( async fn get_flow_job_debug_info( OptAuthed(opt_authed): OptAuthed, + tokened_o: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> error::Result { @@ -561,7 +568,7 @@ async fn get_flow_job_debug_info( } } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), tokened_o.token.as_deref(), &w_id, &id).await?; Ok(Json(jobs).into_response()) } else { @@ -621,6 +628,7 @@ struct GetJobQuery { async fn get_job( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Query(GetJobQuery { no_logs }): Query, @@ -640,7 +648,7 @@ async fn get_job( let mut job = get.fetch(&db, id, &w_id).await?; job.fetch_outstanding_wait_time(&db).await?; - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(job).into_response()) } @@ -1088,6 +1096,7 @@ async fn get_logs_from_disk( async fn get_job_logs( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> error::Result { @@ -1125,7 +1134,7 @@ async fn get_job_logs( } let logs = record.logs.unwrap_or_default(); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; #[cfg(all(feature = "enterprise", feature = "parquet"))] if let Some(r) = get_logs_from_store(record.log_offset, &logs, &record.log_file_index).await @@ -1162,7 +1171,7 @@ async fn get_job_logs( } let logs = text.logs.unwrap_or_default(); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; #[cfg(all(feature = "enterprise", feature = "parquet"))] if let Some(r) = @@ -1186,6 +1195,7 @@ async fn get_job_logs( async fn get_args( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> JsonResult> { @@ -1211,7 +1221,7 @@ async fn get_args( )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(record.args.map(|x| x.0).unwrap_or_default())) } else { @@ -1232,7 +1242,7 @@ async fn get_args( )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(record.args.map(|x| x.0).unwrap_or_default())) } @@ -1993,13 +2003,23 @@ pub async fn resume_suspended_flow_as_owner( pub async fn resume_suspended_job( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>, Query(approver): Query, QueryOrBody(value): QueryOrBody, ) -> error::Result { resume_suspended_job_internal( - value, db, w_id, job_id, resume_id, approver, secret, authed, true, + value, + db, + w_id, + job_id, + resume_id, + approver, + secret, + authed, + opt_tokened, + true, ) .await } @@ -2013,6 +2033,7 @@ async fn resume_suspended_job_internal( approver: QueryApprover, secret: String, authed: Option, + opt_tokened: OptTokened, approved: bool, ) -> Result { let value = value.unwrap_or(serde_json::Value::Null); @@ -2091,6 +2112,7 @@ async fn resume_suspended_job_internal( email: approver.clone(), username: approver.clone(), username_override: None, + token_prefix: opt_tokened.token.map(|s| s[0..TOKEN_PREFIX_LEN].to_string()), }, }; audit_log( @@ -2260,13 +2282,14 @@ async fn get_suspended_flow_info<'c>( pub async fn cancel_suspended_job( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>, Query(approver): Query, QueryOrBody(value): QueryOrBody, ) -> error::Result { resume_suspended_job_internal( - value, db, w_id, job_id, resume_id, approver, secret, authed, false, + value, db, w_id, job_id, resume_id, approver, secret, authed, opt_tokened, false, ) .await } @@ -2284,6 +2307,7 @@ pub struct QueryApprover { pub async fn get_suspended_job_flow( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>, Query(approver): Query, @@ -2350,7 +2374,7 @@ pub async fn get_suspended_job_flow( approvers_from_status }; - log_job_view(&db, authed.as_ref(), &w_id, &job).await?; + log_job_view(&db, authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job).await?; Ok(Json(SuspendedJobFlow { job: flow, approvers }).into_response()) } @@ -3507,6 +3531,7 @@ pub async fn run_flow_by_path_inner( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -3600,6 +3625,7 @@ pub async fn restart_flow( &authed.username, &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -3692,6 +3718,7 @@ pub async fn run_script_by_path_inner( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -3837,6 +3864,7 @@ pub async fn run_workflow_as_code( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, Some(job_id), @@ -4252,6 +4280,7 @@ impl JobViewCache { async fn log_job_view( db: &DB, opt_authed: Option<&ApiAuthed>, + opt_token: Option<&str>, w_id: &str, job_id: &Uuid, ) -> error::Result<()> { @@ -4262,6 +4291,7 @@ async fn log_job_view( username: "anonymous".to_string(), username_override: None, email: "anonymous".to_string(), + token_prefix: opt_token.map(|t| t[0..TOKEN_PREFIX_LEN].to_string()) }, }; if JOB_VIEW_CACHE @@ -4360,6 +4390,7 @@ pub async fn run_wait_result_job_by_path_get( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), None, None, run_query.parent_job, @@ -4500,6 +4531,7 @@ pub async fn run_wait_result_script_by_path_internal( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), None, None, run_query.parent_job, @@ -4613,6 +4645,7 @@ pub async fn run_wait_result_script_by_hash( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), None, None, run_query.parent_job, @@ -4727,6 +4760,7 @@ pub async fn run_wait_result_flow_by_path_internal( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -4797,6 +4831,7 @@ async fn run_preview_script( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, None, @@ -4885,6 +4920,7 @@ async fn run_bundle_preview_script( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, None, @@ -5052,6 +5088,7 @@ async fn run_dependencies_job( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -5109,6 +5146,7 @@ async fn run_flow_dependencies_job( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), None, None, None, @@ -5449,6 +5487,7 @@ async fn run_preview_flow_job( authed.display_username(), &authed.email, username_to_permissioned_as(&authed.username), + authed.token_prefix.as_deref(), scheduled_for, None, None, @@ -5568,6 +5607,7 @@ pub async fn run_job_by_hash_inner( authed.display_username(), email, permissioned_as, + authed.token_prefix.as_deref(), scheduled_for, None, run_query.parent_job, @@ -5660,6 +5700,7 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R async fn get_job_update( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, job_id)): Path<(String, Uuid)>, Query(JobUpdateQuery { log_offset, get_progress, running }): Query, @@ -5716,7 +5757,7 @@ async fn get_job_update( "As a non logged in user, you can only see jobs ran by anonymous users".to_string(), )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &job_id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &job_id).await?; Ok(Json(JobUpdate { running: record.running, completed: record.completed, @@ -6004,6 +6045,7 @@ async fn list_completed_jobs( async fn get_completed_job<'a>( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, ) -> error::Result { @@ -6029,7 +6071,7 @@ async fn get_completed_job<'a>( // .fetch_optional(db) // .await.ok().flatten().flatten(); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(response) } @@ -6043,6 +6085,7 @@ pub struct RawResult { async fn get_completed_job_result( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Query(JsonPath { json_path, suspended_job, approver, resume_id, secret }): Query, @@ -6131,7 +6174,7 @@ async fn get_completed_job_result( raw_result.result.as_mut(), ); - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(raw_result.result).into_response()) } @@ -6189,6 +6232,7 @@ struct GetCompletedJobQuery { async fn get_completed_job_result_maybe( OptAuthed(opt_authed): OptAuthed, + opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, Query(GetCompletedJobQuery { get_started }): Query, @@ -6221,7 +6265,7 @@ async fn get_completed_job_result_maybe( )); } - log_job_view(&db, opt_authed.as_ref(), &w_id, &id).await?; + log_job_view(&db, opt_authed.as_ref(), opt_tokened.token.as_deref(), &w_id, &id).await?; Ok(Json(CompletedJobResult { started: Some(true), @@ -6259,6 +6303,7 @@ async fn get_completed_job_result_maybe( async fn delete_completed_job<'a>( authed: ApiAuthed, + Tokened { token }: Tokened, Extension(user_db): Extension, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, @@ -6308,5 +6353,11 @@ async fn delete_completed_job<'a>( .await?; tx.commit().await?; - return get_completed_job(OptAuthed(Some(authed)), Extension(db), Path((w_id, id))).await; + return get_completed_job( + OptAuthed(Some(authed)), + OptTokened { token: Some(token) }, + Extension(db), + Path((w_id, id)), + ) + .await; } diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 7999356344..fd988a271c 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -508,6 +508,7 @@ pub async fn transform_json_value<'c>( email: "backend".to_string(), username: "backend".to_string(), username_override: None, + token_prefix: None, }), ) .await?; diff --git a/backend/windmill-api/src/schedule.rs b/backend/windmill-api/src/schedule.rs index 3cb13798d9..f5a1aa0fcd 100644 --- a/backend/windmill-api/src/schedule.rs +++ b/backend/windmill-api/src/schedule.rs @@ -7,10 +7,7 @@ */ use crate::{ - db::{ApiAuthed, DB}, - settings::{delete_global_setting, set_global_setting_internal}, - users::maybe_refresh_folders, - utils::require_super_admin, + db::{ApiAuthed, DB}, settings::{delete_global_setting, set_global_setting_internal}, users::maybe_refresh_folders, utils::require_super_admin }; use axum::{ extract::{Extension, Path, Query}, @@ -25,11 +22,7 @@ use std::str::FromStr; use windmill_audit::audit_oss::audit_log; use windmill_audit::ActionKind; use windmill_common::{ - db::UserDB, - error::{Error, JsonResult, Result}, - schedule::Schedule, - utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, - worker::to_raw_value, + db::UserDB, error::{Error, JsonResult, Result}, schedule::Schedule, utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, worker::to_raw_value }; use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; use windmill_queue::schedule::push_scheduled_job; diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 6b74c6381b..83773d4087 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -941,6 +941,7 @@ async fn create_script_internal<'c>( &authed.username, &authed.email, permissioned_as, + authed.token_prefix.as_deref(), None, None, None, diff --git a/backend/windmill-api/src/slack_approvals.rs b/backend/windmill-api/src/slack_approvals.rs index 019f83ecb2..82c1b68d86 100644 --- a/backend/windmill-api/src/slack_approvals.rs +++ b/backend/windmill-api/src/slack_approvals.rs @@ -11,11 +11,11 @@ use std::collections::HashMap; use windmill_common::error::Error; use windmill_common::variables::get_secret_value_as_admin; -use crate::approvals::{ +use crate::{approvals::{ extract_w_id_from_resume_url, handle_resume_action, ApprovalFormDetails, FieldType, MessageFormat, QueryDefaultArgsJson, QueryDynamicEnumJson, QueryFlowStepId, QueryMessage, ResumeFormField, ResumeSchema, -}; +}, auth::OptTokened}; use crate::db::{ApiAuthed, DB}; use crate::jobs::{QueryApprover, ResumeUrls}; @@ -116,6 +116,7 @@ struct PrivateMetadata { pub async fn slack_app_callback_handler( authed: Option, + opt_tokened: OptTokened, Extension(db): Extension, Form(form_data): Form, ) -> Result { @@ -124,8 +125,8 @@ pub async fn slack_app_callback_handler( tracing::debug!("Payload: {:#?}", payload); match payload.r#type { - PayloadType::ViewSubmission => handle_submission(authed, db, &payload, "resume").await?, - PayloadType::ViewClosed => handle_submission(authed, db, &payload, "cancel").await?, + PayloadType::ViewSubmission => handle_submission(authed, opt_tokened, db, &payload, "resume").await?, + PayloadType::ViewClosed => handle_submission(authed, opt_tokened, db, &payload, "cancel").await?, _ => { if let Some(actions) = &payload.actions { if let Some(action) = actions.first() { @@ -256,6 +257,7 @@ pub async fn request_slack_approval( async fn handle_submission( authed: Option, + opt_tokened: OptTokened, db: DB, payload: &Payload, action: &str, @@ -294,7 +296,7 @@ async fn handle_submission( } // Use the common handler to process the resume/cancel action - handle_resume_action(authed, db.clone(), &resume_url, state_json, action).await?; + handle_resume_action(authed, opt_tokened, db.clone(), &resume_url, state_json, action).await?; let w_id = extract_w_id_from_resume_url(&resume_url)?; let slack_token = get_slack_token(&db, &resource_path, w_id).await?; diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index 3a4c2be568..99296c6689 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -44,7 +44,7 @@ use tower_cookies::{Cookie, Cookies}; use tracing::Instrument; use windmill_audit::audit_oss::{audit_log, AuditAuthor}; use windmill_audit::ActionKind; -use windmill_common::auth::fetch_authed_from_permissioned_as; +use windmill_common::auth::{fetch_authed_from_permissioned_as, TOKEN_PREFIX_LEN}; use windmill_common::global_settings::AUTOMATE_USERNAME_CREATION_SETTING; use windmill_common::oauth2::InstanceEvent; use windmill_common::users::COOKIE_NAME; @@ -243,13 +243,14 @@ pub async fn fetch_api_authed_from_permissioned_as( let api_authed = ApiAuthed { username: authed.username, - email: email, + email, is_admin: authed.is_admin, is_operator: authed.is_operator, groups: authed.groups, folders: authed.folders, scopes: authed.scopes, username_override: None, + token_prefix: authed.token_prefix, }; API_AUTHED_CACHE.insert( @@ -690,7 +691,12 @@ async fn logout( }; audit_log( &mut *tx, - &AuditAuthor { email: email.clone(), username: email, username_override: None }, + &AuditAuthor { + email: email.clone(), + username: email, + username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), + }, audit_message, ActionKind::Delete, "global", @@ -1274,7 +1280,10 @@ async fn join_workspace<'c>( Ok((tx, username)) } -async fn leave_instance(Extension(db): Extension, authed: 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", &authed.email) .execute(&mut *tx) @@ -1639,8 +1648,12 @@ async fn login( ) -> Result { let mut tx = db.begin().await?; let email = email.to_lowercase(); - let audit_author = - AuditAuthor { email: email.clone(), username: email.clone(), username_override: None }; + let audit_author = AuditAuthor { + email: email.clone(), + username: email.clone(), + username_override: None, + token_prefix: None, + }; let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as( "SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \ 'password'", @@ -1689,6 +1702,13 @@ async fn login( let token = create_session_token(&email, super_admin, &mut tx, cookies).await?; + let audit_author = AuditAuthor { + email: email.clone(), + username: email.clone(), + username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), + }; + audit_log( &mut *tx, &audit_author, @@ -1758,6 +1778,7 @@ async fn refresh_token( email: authed.email.to_string(), username: authed.email.to_string(), username_override: None, + token_prefix: authed.token_prefix, }, "users.token.refresh", ActionKind::Create, @@ -1798,6 +1819,7 @@ pub async fn create_session_token<'c>( email: email.to_string(), username: email.to_string(), username_override: None, + token_prefix: Some(token[0..TOKEN_PREFIX_LEN].to_string()), }, "users.token.invalidate_old_sessions", ActionKind::Delete, diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 4a6a970db5..73aba91229 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -7,9 +7,7 @@ */ use crate::{ - db::{ApiAuthed, DB}, - users::{maybe_refresh_folders, require_owner_of_path}, - webhook_util::{WebhookMessage, WebhookShared}, + db::{ApiAuthed, DB}, users::{maybe_refresh_folders, require_owner_of_path}, webhook_util::{WebhookMessage, WebhookShared} }; use axum::{ @@ -23,10 +21,7 @@ use serde_json::Value; use windmill_audit::audit_oss::{audit_log, AuditAuthorable}; use windmill_audit::ActionKind; use windmill_common::{ - db::UserDB, - error::{Error, JsonResult, Result}, - utils::{not_found_if_none, paginate, Pagination, StripPath}, - variables::{ + db::UserDB, error::{Error, JsonResult, Result}, utils::{not_found_if_none, paginate, Pagination, StripPath}, variables::{ build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable, }, worker::CLOUD_HOSTED, diff --git a/backend/windmill-audit/src/audit_oss.rs b/backend/windmill-audit/src/audit_oss.rs index d29daaa7ae..f5979f45d7 100644 --- a/backend/windmill-audit/src/audit_oss.rs +++ b/backend/windmill-audit/src/audit_oss.rs @@ -20,14 +20,6 @@ use { }, }; -#[derive(Clone)] -#[cfg(not(feature = "private"))] -pub struct AuditAuthor { - pub username: String, - pub email: String, - pub username_override: Option, -} - #[cfg(not(feature = "private"))] impl AuditAuthorable for AuditAuthor { fn email(&self) -> &str { @@ -41,6 +33,10 @@ impl AuditAuthorable for AuditAuthor { fn username_override(&self) -> Option<&str> { self.username_override.as_deref() } + + fn token_prefix(&self) -> Option<&str> { + self.token_prefix.as_deref() + } } #[cfg(not(feature = "private"))] @@ -48,6 +44,18 @@ pub trait AuditAuthorable { fn username(&self) -> &str; fn email(&self) -> &str; fn username_override(&self) -> Option<&str>; + fn token_prefix(&self) -> Option<&str> { + None + } +} + +#[derive(Clone)] +#[cfg(not(feature = "private"))] +pub struct AuditAuthor { + pub username: String, + pub email: String, + pub username_override: Option, + pub token_prefix: Option, } #[cfg(not(feature = "private"))] diff --git a/backend/windmill-audit/src/lib.rs b/backend/windmill-audit/src/lib.rs index 15bda522c1..1c19890c37 100644 --- a/backend/windmill-audit/src/lib.rs +++ b/backend/windmill-audit/src/lib.rs @@ -24,6 +24,7 @@ pub struct AuditLog { pub action_kind: ActionKind, pub resource: Option, pub parameters: Option, + pub span: Option, } #[derive(Deserialize)] diff --git a/backend/windmill-common/src/auth.rs b/backend/windmill-common/src/auth.rs index 5823fccf7c..f4940793e4 100644 --- a/backend/windmill-common/src/auth.rs +++ b/backend/windmill-common/src/auth.rs @@ -17,6 +17,8 @@ pub struct IdToken { expiration: DateTime, } +pub const TOKEN_PREFIX_LEN: usize = 10; + pub fn has_expired(expiration_time: DateTime, take: Option) -> bool { let now = Utc::now(); @@ -66,6 +68,7 @@ pub struct JWTAuthClaims { pub exp: usize, pub job_id: Option, pub scopes: Option>, + pub audit_span: Option, } #[derive(Deserialize, Debug)] @@ -92,6 +95,7 @@ impl From for Authed { .filter_map(|x| serde_json::from_value::<(String, bool, bool)>(x).ok()) .collect(), scopes: None, + token_prefix: None, } } } @@ -171,38 +175,41 @@ pub async fn fetch_authed_from_permissioned_as( let folders = get_folders_for_user(w_id, &name, &groups, db).await?; Ok(Authed { - email: email, + email, username: name.to_string(), is_admin, is_operator, groups, folders, scopes: None, + token_prefix: None, }) } else { let groups = vec![name.to_string()]; let folders = get_folders_for_user(&w_id, "", &groups, db).await?; Ok(Authed { - email: email, + email, username: format!("group-{name}"), is_admin: false, groups, is_operator: false, folders, scopes: None, + token_prefix: None, }) } } else { let groups = vec![]; let folders = vec![]; Ok(Authed { - email: email, + email, username: permissioned_as, is_admin: super_admin, is_operator: true, groups, folders, scopes: None, + token_prefix: None, }) } } @@ -262,6 +269,7 @@ pub async fn create_token_for_owner( email: &str, job_id: &Uuid, perms: Option, + audit_span: Option, ) -> crate::error::Result { let job_perms = if perms.is_some() { Ok(perms) @@ -302,6 +310,7 @@ pub async fn create_token_for_owner( as usize, job_id: Some(job_id.to_string()), scopes: None, + audit_span, }; let token = jwt::encode_with_internal_secret(&payload) diff --git a/backend/windmill-common/src/db.rs b/backend/windmill-common/src/db.rs index 580e14f9d9..fe0d215c69 100644 --- a/backend/windmill-common/src/db.rs +++ b/backend/windmill-common/src/db.rs @@ -12,6 +12,7 @@ pub struct Authed { // (folder name, can write, is owner) pub folders: Vec<(String, bool, bool)>, pub scopes: Option>, + pub token_prefix: Option, } #[derive(Clone)] diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index fe2e08d0a1..26bc231e5b 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -428,6 +428,7 @@ pub async fn push_init_job<'c>( worker_name, "worker@windmill.dev", SUPERADMIN_SECRET_EMAIL.to_string(), + Some("worker_init_job"), None, None, None, @@ -1212,6 +1213,7 @@ pub async fn add_completed_job( &queued_job.created_by, &queued_job.permissioned_as_email, queued_job.permissioned_as.clone(), + Some(&format!("add.completed.job{}", queued_job.id)), scheduled_for, queued_job.schedule_path(), None, @@ -1735,6 +1737,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>( }, email, permissioned_as, + Some(&format!("error.handler.{job_id}")), None, None, Some(job_id), @@ -1842,6 +1845,7 @@ async fn handle_recovered_schedule<'a, 'c, T: Serialize + Send + Sync>( SCHEDULE_RECOVERY_HANDLER_USERNAME, email, permissioned_as, + Some(&format!("recovered.schedule.{job_id}")), None, None, Some(job_id), @@ -1930,6 +1934,7 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>( SCHEDULE_RECOVERY_HANDLER_USERNAME, email, permissioned_as, + Some(&format!("successful.schedule.recovery{job_id}")), None, None, Some(job_id), @@ -2220,6 +2225,7 @@ pub async fn create_token(db: &DB, job: &MiniPulledJob, perms: Option) &job.permissioned_as_email, &job.id, perms, + Some(format!("job-span-{}", job.flow_innermost_root_job.unwrap_or(job.id))), ) .warn_after_seconds(5) .await @@ -3257,6 +3263,7 @@ pub async fn push<'c, 'd>( user: &str, mut email: &str, mut permissioned_as: String, + token_prefix: Option<&str>, scheduled_for_o: Option>, schedule_path: Option, parent_job: Option, @@ -4372,12 +4379,14 @@ pub async fn push<'c, 'd>( email: email.to_string(), username: permissioned_as.trim_start_matches("u/").to_string(), username_override: Some(user.to_string()), + token_prefix: token_prefix.map(|s| s.to_string()), } } else { AuditAuthor { email: email.to_string(), username: user.to_string(), username_override: None, + token_prefix: token_prefix.map(|s| s.to_string()), } }; @@ -4700,4 +4709,4 @@ pub async fn get_same_worker_job( same_worker_job.job_id, e )) }) -} \ No newline at end of file +} diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index 294ece7509..f539ac7818 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -271,6 +271,7 @@ pub async fn push_scheduled_job<'c>( &schedule_to_user(&schedule.path), email, permissioned_as, + Some(&schedule.path), Some(next), Some(schedule.path.clone()), None, diff --git a/backend/windmill-worker/src/result_processor.rs b/backend/windmill-worker/src/result_processor.rs index 5056851389..ce314a00e6 100644 --- a/backend/windmill-worker/src/result_processor.rs +++ b/backend/windmill-worker/src/result_processor.rs @@ -17,12 +17,7 @@ use windmill_common::otel_oss::FutureExt; use uuid::Uuid; use windmill_common::{ - add_time, - error::{self, Error}, - jobs::JobKind, - utils::WarnAfterExt, - worker::{to_raw_value, Connection, WORKER_GROUP}, - KillpillSender, DB, + add_time, error::{self, Error}, jobs::JobKind, utils::WarnAfterExt, worker::{to_raw_value, Connection, WORKER_GROUP}, KillpillSender, DB }; #[cfg(feature = "benchmark")] diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 95ffc28062..98c292b97a 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -17,6 +17,7 @@ use crate::worker_utils::get_tag_and_concurrency; use crate::{ JobCompletedSender, PreviousResult, SameWorkerSender, SendResult, UpdateFlow, KEEP_JOB_DIR, }; + use anyhow::Context; use futures::TryFutureExt; use mappable_rc::Marc; @@ -2030,6 +2031,7 @@ async fn push_next_flow_job( .to_string(), email: flow_job.permissioned_as_email.clone(), username_override: None, + token_prefix: Some(format!("psh.nxt.flowjob-{}", client.token.to_string())), }; if can_be_resumed || disapproved_or_timeout_but_continue { @@ -2772,6 +2774,7 @@ async fn push_next_flow_job( &flow_job.created_by, email, permissioned_as, + Some(&format!("job-span-{}", flow_job.flow_innermost_root_job.unwrap_or(flow_job.id))), scheduled_for_o, flow_job.schedule_path(), Some(flow_job.id), diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 7fee5932e4..e24ca1972a 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -608,6 +608,7 @@ async fn trigger_dependents_to_recompute_dependencies( &created_by, email, permissioned_as.to_string(), + Some("trigger.dependents.to.recompute.dependencies"), None, None, None, diff --git a/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte b/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte index ff0198ccf7..c0582ffc44 100644 --- a/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte +++ b/frontend/src/lib/components/auditLogs/AuditLogsFilters.svelte @@ -97,6 +97,7 @@ actionKind: ActionKind | undefined | 'all', scope: undefined | 'all_workspaces' | 'instance' ): Promise { + console.log("loading logs") loading = true if (username == 'all') { @@ -130,6 +131,7 @@ hasMore = logs.length > 0 && logs.length === perPage loading = false + console.log("loadede logs") } async function loadUsers() { @@ -353,24 +355,32 @@ After { + on:change={({ detail }) => { after = new Date(detail).toISOString() }} + on:clear={() => { + after = undefined + }} />
Before { + on:change={({ detail }) => { before = new Date(detail).toISOString() }} + on:clear={() => { + before = undefined + }} />
diff --git a/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte b/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte index 601fb9c567..ba98b6259a 100644 --- a/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte +++ b/frontend/src/lib/components/auditLogs/AuditLogsTable.svelte @@ -1,24 +1,38 @@ - { - pageIndex = (pageIndex ?? 1) + 1 - }} - on:previous={() => { - pageIndex = (pageIndex ?? 1) - 1 - }} - currentPage={pageIndex} - paginated - rounded={false} - size="sm" - {hasMore} - bind:perPage + computeHeight()} /> + +
- - ID - Timestamp - Username - Operation - Resource - - {#if logs?.length > 0} - - {#each Object.entries(groupedLogs) as [date, logsByDay]} - - - {date} - - - {#each logsByDay as { id, timestamp, username, operation: op, action_kind, resource, parameters }} - { - dispatch('select', id) - }} - > - - {id} - - - {displayDate(timestamp)} - - -
-
- {username} - {#if parameters && 'end_user' in parameters} - ({parameters.end_user}) - {/if} -
-
-
- -
- { - actionKind = action_kind.toLocaleLowerCase() - }} - color={kindToBadgeColor(action_kind)}>{action_kind} - { - operation = op - }} - > - {op} - -
-
- -
-
- {resource} -
-
-
-
- {/each} - {/each} - + +
+
+
ID
+
Timestamp
+
Username
+
Operation
+
Resource
+
+
+ {#if logs?.length == 0} +
No logs found for the selected filters.
{:else} - - -
No logs found for the selected filters.
- - + + {#snippet header()}{/snippet} + {#snippet children({ index, style })} +
+ {#if flatLogs} + {@const logOrDate = flatLogs[index]} + + {#if logOrDate} + {#if logOrDate?.type === 'date'} +
+ {logOrDate.date} +
+ {:else} + + +
{ + onselect?.(logOrDate.log.id) + }} + > +
+ {logOrDate.log.id} +
+
+ {displayDate(logOrDate.log.timestamp)} +
+
+
+
+ {logOrDate.log.username} + {#if logOrDate.log.parameters && 'end_user' in logOrDate.log.parameters} + ({logOrDate.log.parameters.end_user}) + {/if} +
+
+
+
+
+ { + actionKind = logOrDate.log.action_kind.toLocaleLowerCase() + }} + color={kindToBadgeColor(logOrDate.log.action_kind)} + > + {logOrDate.log.action_kind} + + { + operation = logOrDate.log.operation + }} + > + {logOrDate.log.operation} + +
+
+
+
+
+ {logOrDate.log.resource} +
+
+
+
+ {/if} + {:else} +
+
Loading...
+
+ {/if} + {:else} +
+
Loading...
+
+ {/if} +
+ {/snippet} + {#snippet footer()}{/snippet} +
{/if} - + +
+
+ + Page {pageIndex} + +
+
+ Per page: + +
+
+
diff --git a/frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte b/frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte new file mode 100644 index 0000000000..e2f7fc2365 --- /dev/null +++ b/frontend/src/lib/components/auditLogs/AuditLogsTimeline.svelte @@ -0,0 +1,564 @@ + + +
+ {#if logs.length === 0} +
No audit logs to display
+ {:else if !groupedData || groupedData.status === 'loading'} +
+ + Processing audit logs... +
+ {:else} + + {/if} +
diff --git a/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte b/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte index e28cbbb20c..30fd36ace4 100644 --- a/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/audit_logs/+page.svelte @@ -1,40 +1,82 @@ {#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.audit_logs} @@ -43,15 +85,15 @@

Page not available for operators

{:else} -
-
-
-
-

Audit logs

- - You can only see your own audit logs unless you are an admin. - -
+
+
+
+

Audit logs

+ + You can only see your own audit logs unless you are an admin. + +
+
- - {#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')} - - You need an enterprise license to see unredacted audit logs. - -
+
+
+ {#if logs} + { + before = max.toISOString() + after = min.toISOString() + console.log('zoom!') + }} + onMissingJobSpan={fetchMissingJobSpan} + onLogSelected={(log) => { + console.log('selected log ') + selectedId = log.id + }} + /> {/if}
- +
+
+ {#if !$enterpriseLicense || $enterpriseLicense.endsWith('_pro')} + + You need an enterprise license to see unredacted audit logs. + +
+ {/if} +
+ + + + {#if logs} + { + selectedId = id + }} + /> + {:else} +
+ {#each new Array(8) as _} + + {/each} +
+ {/if} +
+ + {#if logs} + + {/if} + +
+
-
- { - selectedId = e.detail - - auditLogDrawer?.openDrawer() - }} - /> +
+ { + selectedId = id + auditLogDrawer?.openDrawer() + }} + /> +
{/if}