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 afae33f5f8.

* feat: add username to email function

* fix: nit

* fix: add docstring to global func
This commit is contained in:
HugoCasa
2024-05-23 17:23:50 +02:00
committed by GitHub
parent e5b5d72a6f
commit 126459f972
33 changed files with 426 additions and 27186 deletions
@@ -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"
}
@@ -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"
}
@@ -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"
}
+1 -1
View File
@@ -1 +1 @@
229b7acc60691f7e6577c44f782676d6871defdd
cb38edaf31affeda48317433d10903245aa518f2
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE token ALTER COLUMN label TYPE VARCHAR(1000)
+6 -1
View File
@@ -1079,11 +1079,16 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
mpsc::channel::<SameWorkerPayload>(1);
let (send_result_never_used, _send_result_rx_never_used) = mpsc::channel::<SendResult>(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,
+21
View File
@@ -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
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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",
+23
View File
@@ -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<ApiAuthed> 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
+4 -4
View File
@@ -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,
+5 -5
View File
@@ -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,
+10 -10
View File
@@ -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,
+125 -89
View File
@@ -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<String> {
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<CancelJob>,
) -> 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<String> {
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<ListCompletedQuery> 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,
+1 -1
View File
@@ -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,
+4 -6
View File
@@ -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,
+16 -11
View File
@@ -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,
+5 -5
View File
@@ -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,
+6 -6
View File
@@ -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,
+8 -8
View File
@@ -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<String> {
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<String> {
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,
+65 -53
View File
@@ -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<String> {
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<DB>,
Path(w_id): Path<String>,
ApiAuthed { is_admin, username, email, .. }: ApiAuthed,
authed: ApiAuthed,
) -> Result<String> {
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<DB>,
Path(w_id): Path<String>,
ApiAuthed { email, username, .. }: ApiAuthed,
authed: ApiAuthed,
) -> Result<String> {
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<DB>,
Path(w_id): Path<String>,
ApiAuthed { is_admin, username, email, .. }: ApiAuthed,
authed: ApiAuthed,
) -> Result<String> {
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<DB>,
Path(w_id): Path<String>,
ApiAuthed { username, email, .. }: ApiAuthed,
authed: ApiAuthed,
) -> Result<String> {
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<rsmq_async::MultiplexedRsmq>,
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<DB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(mut nu): Json<NewWorkspaceUser>,
) -> 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,
+28 -1
View File
@@ -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<String>,
}
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,
+16 -2
View File
@@ -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,
+9 -1
View File
@@ -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,
@@ -92,7 +92,7 @@
{date}
</Cell>
</tr>
{#each logsByDay as { id, timestamp, username, operation: op, action_kind, resource }}
{#each logsByDay as { id, timestamp, username, operation: op, action_kind, resource, parameters }}
<Row
hoverable
selected={id === selectedId}
@@ -108,8 +108,11 @@
</Cell>
<Cell>
<div class="flex flex-row gap-2 items-center">
<div class="whitespace-nowrap overflow-x-auto no-scrollbar w-20">
<div class="whitespace-nowrap overflow-x-auto no-scrollbar w-48">
{username}
{#if parameters && 'end_user' in parameters}
<span> (end user: {parameters.end_user})</span>
{/if}
</div>
<Button
color="light"
+28 -13
View File
@@ -142,9 +142,7 @@ class Windmill:
timeout = timeout.total_seconds()
job_id = self.run_script_async(path=path, hash_=hash_, args=args)
return self.wait_job(
job_id, timeout, verbose, cleanup, assert_result_is_not_none
)
return self.wait_job(job_id, timeout, verbose, cleanup, assert_result_is_not_none)
def wait_job(
self,
@@ -183,7 +181,7 @@ class Windmill:
atexit.unregister(cancel_job)
if completed:
result = result_res["result"]
result = result_res["result"]
if success:
if result is None and assert_result_is_not_none:
raise Exception("Result was none")
@@ -203,10 +201,8 @@ class Windmill:
if verbose:
logger.info(f"sleeping 0.5 seconds for {job_id = }")
time.sleep(0.5)
def cancel_running(self) -> dict:
"""Cancel currently running executions of the same script."""
logger.info("canceling running executions of this script")
@@ -247,7 +243,6 @@ class Windmill:
job_id = job_id or os.environ.get("WM_JOB_ID")
return self.get(f"/w/{self.workspace}/jobs_u/get_root_job_id/{job_id}").json()
def get_id_token(self, audience: str) -> str:
return self.post(f"/w/{self.workspace}/oidc/token/{audience}").text
@@ -349,11 +344,10 @@ class Windmill:
def set_flow_user_state(self, key: str, value: Any) -> None:
"""Set the user state of a flow at a given key"""
flow_id = self.get_root_job_id()
r = self.post(f"/w/{self.workspace}/jobs/flow/user_states/{flow_id}/{key}", json=value, raise_for_status=False)
r = self.post(f"/w/{self.workspace}/jobs/flow/user_states/{flow_id}/{key}", json=value, raise_for_status=False)
if r.status_code == 404:
print(f"Job {flow_id} does not exist or is not a flow")
def get_flow_user_state(self, key: str) -> Any:
"""Get the user state of a flow at a given key"""
flow_id = self.get_root_job_id()
@@ -568,7 +562,7 @@ class Windmill:
@staticmethod
def get_shared_state(path: str = "state.json") -> None:
"""
Set the state in the shared folder using pickle
Get the state in the shared folder using pickle
"""
import json
@@ -583,6 +577,14 @@ class Windmill:
params={"approver": approver},
).json()
def username_to_email(self, username: str) -> str:
"""
Get email from workspace username
This method is particularly useful for apps that require the email address of the viewer.
Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
"""
return self.get(f"/w/{self.workspace}/users/username_to_email/{username}").text
def init_global_client(f):
@functools.wraps(f)
@@ -617,6 +619,7 @@ def deprecate(in_favor_of: str):
def get_workspace() -> str:
return _client.workspace
@init_global_client
def get_root_job_id(job_id: str | None = None) -> str:
return _client.get_root_job_id(job_id)
@@ -848,7 +851,7 @@ def set_shared_state(value: Any, path="state.json") -> None:
def get_shared_state(path="state.json") -> None:
"""
Set the state in the shared folder using pickle
Get the state in the shared folder using pickle
"""
return Windmill.get_shared_state(path=path)
@@ -876,6 +879,7 @@ def get_flow_user_state(key: str) -> Any:
"""
return _client.get_flow_user_state(key)
@init_global_client
def set_flow_user_state(key: str, value: Any) -> None:
"""
@@ -922,10 +926,22 @@ def run_script(
)
@init_global_client
def username_to_email(username: str) -> str:
"""
Get email from workspace username
This method is particularly useful for apps that require the email address of the viewer.
Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
"""
return _client.username_to_email(username)
def task(*args, **kwargs):
from inspect import signature
def f(func, tag: str | None = None):
if os.environ.get("WM_JOB_ID") is None or os.environ.get("MAIN_OVERRIDE") == func.__name__:
def inner(*args, **kwargs):
return func(*args, **kwargs)
@@ -963,9 +979,8 @@ def task(*args, **kwargs):
return r
return inner
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
return f(args[0], None)
else:
return lambda x: f(x, kwargs.get("tag"))
+1 -1
View File
@@ -14,5 +14,5 @@ cp "${script_dirpath}/s3Types.ts" "${script_dirpath}/src/"
echo "" >> "${script_dirpath}/src/index.ts"
echo 'export type { S3Object, DenoS3LightClientSettings } from "./s3Types";' >> "${script_dirpath}/src/index.ts"
echo "" >> "${script_dirpath}/src/index.ts"
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState } from "./client";' >> "${script_dirpath}/src/index.ts"
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail } from "./client";' >> "${script_dirpath}/src/index.ts"
+1 -1
View File
@@ -31,4 +31,4 @@ cp "${script_dirpath}/s3Types.ts" "${script_dirpath}/src/"
echo "" >> "${script_dirpath}/src/index.ts"
echo 'export type { S3Object, DenoS3LightClientSettings } from "./s3Types";' >> "${script_dirpath}/src/index.ts"
echo "" >> "${script_dirpath}/src/index.ts"
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState } from "./client";' >> "${script_dirpath}/src/index.ts"
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail } from "./client";' >> "${script_dirpath}/src/index.ts"
+13
View File
@@ -4,6 +4,7 @@ import {
JobService,
HelpersService,
OidcService,
UserService,
} from "./index";
import { OpenAPI } from "./index";
// import type { DenoS3LightClientSettings } from "./index";
@@ -714,3 +715,15 @@ export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string {
return base64;
}
/**
* Get email from workspace username
* This method is particularly useful for apps that require the email address of the viewer.
* Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
* @param username
* @returns email address
*/
export async function usernameToEmail(username: string): Promise<string> {
const workspace = getWorkspace();
return await UserService.usernameToEmail({ username, workspace });
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-client",
"version": "1.326.0",
"version": "1.335.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-client",
"version": "1.326.0",
"version": "1.335.0",
"license": "Apache 2.0",
"devDependencies": {
"@types/node": "^20.4.10",
-16966
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff