diff --git a/backend/.sqlx/query-5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00.json b/backend/.sqlx/query-5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00.json new file mode 100644 index 0000000000..82f052da9f --- /dev/null +++ b/backend/.sqlx/query-5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT email FROM usr WHERE username = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00" +} diff --git a/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json b/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json index 75b8108281..1fa370e682 100644 --- a/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json +++ b/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json @@ -5,7 +5,7 @@ "columns": [ { "ordinal": 0, - "name": "?column?", + "name": "bool", "type_info": "Bool" } ], diff --git a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json index 54e94cfb8f..99269c9851 100644 --- a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json +++ b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json @@ -18,8 +18,8 @@ "Left": [] }, "nullable": [ - false, - true + true, + false ] }, "hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76" diff --git a/backend/.sqlx/query-da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a.json b/backend/.sqlx/query-da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a.json new file mode 100644 index 0000000000..c4c166cd80 --- /dev/null +++ b/backend/.sqlx/query-da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a.json @@ -0,0 +1,23 @@ +{ + "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 197cb90f1e..3ee24208ae 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -e632d5f821ee229823288b30b7ef5d3e59db07fa \ No newline at end of file +93bf43cfcf05ed285e05ec07267765a5c7d888a3 \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 44c6a7cf5b..ad7ea728cb 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -9776,7 +9776,13 @@ components: properties: type: type: string - enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"] + enum: + [ + "S3Storage", + "AzureBlobStorage", + "AzureWorkloadIdentity", + "S3AwsOidc", + ] s3_resource_path: type: string azure_blob_resource_path: @@ -9866,6 +9872,8 @@ components: - secret - resourcetype - schedule + - user + - group repositories: type: array items: @@ -9912,6 +9920,8 @@ components: - secret - resourcetype - schedule + - user + - group required: - script_path - git_repo_resource_path diff --git a/backend/windmill-api/src/groups.rs b/backend/windmill-api/src/groups.rs index 1146c659e0..e7d8063ea7 100644 --- a/backend/windmill-api/src/groups.rs +++ b/backend/windmill-api/src/groups.rs @@ -25,6 +25,7 @@ use windmill_common::{ use serde::{Deserialize, Serialize}; use sqlx::{query_scalar, FromRow, Postgres, Transaction}; +use windmill_git_sync::handle_deployment_metadata; pub fn workspaced_service() -> Router { Router::new() @@ -219,6 +220,7 @@ async fn create_group( authed: ApiAuthed, Extension(_db): Extension, Extension(user_db): Extension, + Extension(rsmq): Extension>, Path(w_id): Path, Json(ng): Json, ) -> Result { @@ -260,6 +262,19 @@ async fn create_group( .await?; tx.commit().await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &_db, + &w_id, + windmill_git_sync::DeployedObject::Group { name: ng.name.clone() }, + Some(format!("Created group '{}'", &ng.name)), + rsmq, + true, + ) + .await?; + Ok(format!("Created group {}", ng.name)) } @@ -424,6 +439,7 @@ async fn delete_group( authed: ApiAuthed, Extension(db): Extension, Extension(user_db): Extension, + Extension(rsmq): Extension>, Path((w_id, name)): Path<(String, String)>, ) -> Result { let mut tx = user_db.begin(&authed).await?; @@ -465,6 +481,19 @@ async fn delete_group( ) .await?; tx.commit().await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &w_id, + windmill_git_sync::DeployedObject::Group { name: name.clone() }, + Some(format!("Deleted group '{}'", &name)), + rsmq, + true, + ) + .await?; + Ok(format!("delete group at name {}", name)) } @@ -472,6 +501,7 @@ async fn update_group( authed: ApiAuthed, Extension(db): Extension, Extension(user_db): Extension, + Extension(rsmq): Extension>, Path((w_id, name)): Path<(String, String)>, Json(eg): Json, ) -> Result { @@ -501,6 +531,19 @@ async fn update_group( ) .await?; tx.commit().await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &w_id, + windmill_git_sync::DeployedObject::Group { name: name.clone() }, + Some(format!("Updated group '{}'", &name)), + rsmq, + true, + ) + .await?; + Ok(format!("Edited group {}", name)) } @@ -508,6 +551,7 @@ async fn add_user( authed: ApiAuthed, Extension(db): Extension, Extension(user_db): Extension, + Extension(rsmq): Extension>, Path((w_id, name)): Path<(String, String)>, Json(Username { username: user_username }): Json, ) -> Result { @@ -538,6 +582,19 @@ async fn add_user( ) .await?; tx.commit().await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &w_id, + windmill_git_sync::DeployedObject::Group { name: name.clone() }, + Some(format!("Added user to group '{}'", &name)), + rsmq, + true, + ) + .await?; + Ok(format!("Added {} to group {}", user_username, name)) } @@ -652,6 +709,7 @@ async fn remove_user( authed: ApiAuthed, Extension(db): Extension, Extension(user_db): Extension, + Extension(rsmq): Extension>, Path((w_id, name)): Path<(String, String)>, Json(Username { username: user_username }): Json, ) -> Result { @@ -685,5 +743,18 @@ async fn remove_user( .await?; tx.commit().await?; + + handle_deployment_metadata( + &authed.email, + &authed.username, + &db, + &w_id, + windmill_git_sync::DeployedObject::Group { name: name.clone() }, + Some(format!("Removed user from group '{}'", &name)), + rsmq, + true, + ) + .await?; + Ok(format!("Removed {} to group {}", user_username, name)) } diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index 84220391e7..f8d40e26d0 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -53,6 +53,7 @@ use windmill_common::{ users::SUPERADMIN_SECRET_EMAIL, utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath}, }; +use windmill_git_sync::handle_deployment_metadata; pub const TTL_TOKEN_DB_H: u32 = 72; @@ -1304,6 +1305,7 @@ async fn accept_invite( ApiAuthed { email, .. }: ApiAuthed, Extension(webhook): Extension, Extension(db): Extension, + Extension(rsmq): Extension>, Json(nu): Json, ) -> Result<(StatusCode, String)> { let mut tx = db.begin().await?; @@ -1316,7 +1318,6 @@ async fn accept_invite( .fetch_optional(&mut *tx) .await?; - // let mut username = nu.username; if let Some(r) = r { let username; (tx, username) = add_user_to_workspace( @@ -1340,6 +1341,18 @@ async fn accept_invite( ) .await?; tx.commit().await?; + + handle_deployment_metadata( + &email, + &username, + &db, + &nu.workspace_id, + windmill_git_sync::DeployedObject::User { email: email.clone() }, + Some(format!("User '{}' accepted invite", &email)), + rsmq, + true, + ) + .await?; webhook.send_instance_event(InstanceEvent::UserJoinedWorkspace { email: email.clone(), workspace: nu.workspace_id.clone(), @@ -1504,8 +1517,9 @@ async fn get_workspace_user( } async fn update_workspace_user( - ApiAuthed { username, is_admin, .. }: ApiAuthed, + ApiAuthed { username, email, is_admin, .. }: ApiAuthed, Extension(db): Extension, + Extension(rsmq): Extension>, Path((w_id, username_to_update)): Path<(String, String)>, Json(eu): Json, ) -> Result { @@ -1556,7 +1570,29 @@ async fn update_workspace_user( None, ) .await?; + + let user_email = sqlx::query_scalar!( + "SELECT email FROM usr WHERE username = $1 AND workspace_id = $2", + &username_to_update, + &w_id + ) + .fetch_one(&mut *tx) + .await?; + tx.commit().await?; + + handle_deployment_metadata( + &email, + &username, + &db, + &w_id, + windmill_git_sync::DeployedObject::User { email: user_email.clone() }, + Some(format!("Updated user '{}'", &user_email)), + rsmq, + true, + ) + .await?; + Ok(format!("user {username} updated")) } @@ -1652,6 +1688,7 @@ async fn create_user( Extension(db): Extension, Extension(webhook): Extension, Extension(argon2): Extension>>, + Extension(rsmq): Extension>, Json(mut nu): Json, ) -> Result<(StatusCode, String)> { require_super_admin(&db, &email).await?; @@ -1726,7 +1763,7 @@ async fn create_user( tx.commit().await?; - invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?; + invite_user_to_all_auto_invite_worspaces(&db, &nu.email, rsmq).await?; send_email_if_possible( "Invited to Windmill", &format!( @@ -1790,8 +1827,9 @@ pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: &st } async fn delete_workspace_user( - ApiAuthed { username, is_admin, .. }: ApiAuthed, + ApiAuthed { username, email, is_admin, .. }: ApiAuthed, Extension(db): Extension, + Extension(rsmq): Extension>, Path((w_id, username_to_delete)): Path<(String, String)>, ) -> Result { let mut tx = db.begin().await?; @@ -1835,6 +1873,22 @@ async fn delete_workspace_user( ) .await?; tx.commit().await?; + + handle_deployment_metadata( + &email, + &username, + &db, + &w_id, + windmill_git_sync::DeployedObject::User { email: email_to_delete.clone() }, + Some(format!( + "Removed user '{}' from workspace", + &email_to_delete + )), + rsmq, + true, + ) + .await?; + Ok(format!("username {} deleted", username_to_delete)) } diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index b9ea884c66..26202390ac 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -53,6 +53,7 @@ use windmill_common::{ utils::{paginate, rd_string, require_admin, Pagination}, variables::ExportableListableVariable, }; +use windmill_git_sync::handle_deployment_metadata; use windmill_queue::QueueTransaction; use crate::oauth2_ee::InstanceEvent; @@ -634,6 +635,7 @@ async fn auto_add_user( async fn edit_auto_invite( authed: ApiAuthed, Extension(db): Extension, + Extension(rsmq): Extension>, Path(w_id): Path, ApiAuthed { is_admin, email, username, .. }: ApiAuthed, Json(ea): Json, @@ -661,6 +663,8 @@ async fn edit_auto_invite( let mut tx = db.begin().await?; + let mut users_to_auto_add = Option::None; + if let (Some(operator), Some(auto_add)) = (ea.operator, ea.auto_add) { if BANNED_DOMAINS.contains(domain) { return Err(Error::BadRequest(format!( @@ -680,16 +684,16 @@ async fn edit_auto_invite( .await?; if auto_add { - let users = sqlx::query!( + users_to_auto_add = Some(sqlx::query!( "SELECT email FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS ( SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email )", &w_id, domain ) - .fetch_all(&mut *tx).await?; + .fetch_all(&mut *tx).await?); - for user in users { + for user in users_to_auto_add.as_ref().unwrap() { auto_add_user(&user.email, &w_id, &operator, &mut tx).await?; } } else { @@ -727,6 +731,22 @@ async fn edit_auto_invite( .await?; tx.commit().await?; + if let Some(users) = users_to_auto_add { + for user in users { + handle_deployment_metadata( + &email, + &username, + &db, + &w_id, + windmill_git_sync::DeployedObject::User { email: user.email.clone() }, + Some(format!("Auto-added user '{}' to workspace", &user.email)), + rsmq.clone(), + true, + ) + .await?; + } + } + Ok(format!( "Edit auto-invite for workspace {} to {}", &w_id, domain @@ -1751,7 +1771,11 @@ async fn delete_workspace( Ok(format!("Deleted workspace {}", &w_id)) } -pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> Result<()> { +pub async fn invite_user_to_all_auto_invite_worspaces( + db: &DB, + email: &str, + rsmq: Option, +) -> Result<()> { let mut tx = db.begin().await?; let domain = email.split('@').last().unwrap(); let workspaces = sqlx::query!( @@ -1761,10 +1785,19 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R ) .fetch_all(&mut *tx) .await?; + let mut auto_added_workspace_usernames: Vec<(String, String)> = vec![]; 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?; + auto_added_workspace_usernames.push((r.workspace_id, username)); } else { sqlx::query!( "INSERT INTO workspace_invite @@ -1780,6 +1813,21 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R } } tx.commit().await?; + + for workspace_username_tuple in auto_added_workspace_usernames { + let (w_id, username) = workspace_username_tuple; + handle_deployment_metadata( + &email, + &username, + db, + &w_id, + windmill_git_sync::DeployedObject::User { email: email.to_string() }, + Some(format!("Auto-added user '{}' to workspace", email)), + rsmq.clone(), + true, + ) + .await?; + } Ok(()) } @@ -1837,6 +1885,7 @@ async fn add_user( ApiAuthed { username, email, is_admin, .. }: ApiAuthed, Extension(db): Extension, Extension(webhook): Extension, + Extension(rsmq): Extension>, Path(w_id): Path, Json(mut nu): Json, ) -> Result<(StatusCode, String)> { @@ -1928,6 +1977,18 @@ async fn add_user( tx.commit().await?; + handle_deployment_metadata( + &email, + &username, + &db, + &w_id, + windmill_git_sync::DeployedObject::User { email: nu.email.clone() }, + Some(format!("Added user '{}' to workspace", &nu.email)), + rsmq, + true, + ) + .await?; + send_email_if_possible( &format!("Added to Windmill's workspace: {w_id}"), &format!( diff --git a/backend/windmill-common/src/workspaces.rs b/backend/windmill-common/src/workspaces.rs index 7f9f74ecd8..9e9cfc2713 100644 --- a/backend/windmill-common/src/workspaces.rs +++ b/backend/windmill-common/src/workspaces.rs @@ -19,6 +19,8 @@ pub enum ObjectType { Secret, Schedule, ResourceType, + User, + Group, } #[derive(Serialize, Deserialize, Debug)] diff --git a/backend/windmill-git-sync/src/lib.rs b/backend/windmill-git-sync/src/lib.rs index 8b979a9249..7c34efcc8c 100644 --- a/backend/windmill-git-sync/src/lib.rs +++ b/backend/windmill-git-sync/src/lib.rs @@ -25,19 +25,30 @@ pub enum DeployedObject { Variable { path: String, parent_path: Option }, Schedule { path: String }, ResourceType { path: String }, + User { email: String }, + Group { name: String }, } impl DeployedObject { - pub fn get_path(&self) -> &str { + pub fn get_path(&self) -> String { match self { - DeployedObject::Script { path, .. } => path, - DeployedObject::Flow { path, .. } => path, - DeployedObject::App { path, .. } => path, - DeployedObject::Folder { path, .. } => path, - DeployedObject::Resource { path, .. } => path, - DeployedObject::Variable { path, .. } => path, - DeployedObject::Schedule { path, .. } => path, - DeployedObject::ResourceType { path, .. } => path, + DeployedObject::Script { path, .. } => path.to_owned(), + DeployedObject::Flow { path, .. } => path.to_owned(), + DeployedObject::App { path, .. } => path.to_owned(), + DeployedObject::Folder { path, .. } => path.to_owned(), + DeployedObject::Resource { path, .. } => path.to_owned(), + DeployedObject::Variable { path, .. } => path.to_owned(), + DeployedObject::Schedule { path, .. } => path.to_owned(), + DeployedObject::ResourceType { path, .. } => path.to_owned(), + DeployedObject::User { email } => format!("users/{email}"), + DeployedObject::Group { name } => format!("groups/{name}"), + } + } + + pub fn get_ignore_regex_filter(&self) -> bool { + match self { + DeployedObject::User { .. } | DeployedObject::Group { .. } => true, + _ => false, } } @@ -51,6 +62,8 @@ impl DeployedObject { DeployedObject::Variable { parent_path, .. } => parent_path.to_owned(), DeployedObject::Schedule { .. } => None, DeployedObject::ResourceType { .. } => None, + DeployedObject::User { .. } => None, + DeployedObject::Group { .. } => None, } } } diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 8226b03002..feb6c2c078 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -62,6 +62,8 @@ variables: boolean secrets: boolean schedules: boolean + users: boolean + groups: boolean } type GitSyncType = | 'script' @@ -73,6 +75,8 @@ | 'variable' | 'secret' | 'schedule' + | 'user' + | 'group' let s3FileViewer: S3FilePicker @@ -329,6 +333,12 @@ if (typesMap.schedules == expectedValue) { result.push('schedule') } + if (typesMap.users == expectedValue) { + result.push('user') + } + if (typesMap.groups == expectedValue) { + result.push('group') + } return result } @@ -479,7 +489,9 @@ variables: (settings.exclude_types_override?.indexOf('variable') ?? -1) >= 0, secrets: (settings.exclude_types_override?.indexOf('secret') ?? -1) >= 0, schedules: (settings.exclude_types_override?.indexOf('schedule') ?? -1) >= 0, - folders: (settings.exclude_types_override?.indexOf('folder') ?? -1) >= 0 + folders: (settings.exclude_types_override?.indexOf('folder') ?? -1) >= 0, + users: (settings.exclude_types_override?.indexOf('user') ?? -1) >= 0, + groups: (settings.exclude_types_override?.indexOf('group') ?? -1) >= 0 } } }), @@ -492,7 +504,9 @@ variables: (settings.git_sync.include_type?.indexOf('variable') ?? -1) >= 0, secrets: (settings.git_sync.include_type?.indexOf('secret') ?? -1) >= 0, schedules: (settings.git_sync.include_type?.indexOf('schedule') ?? -1) >= 0, - folders: (settings.git_sync.include_type?.indexOf('folder') ?? -1) >= 0 + folders: (settings.git_sync.include_type?.indexOf('folder') ?? -1) >= 0, + users: (settings.git_sync.include_type?.indexOf('user') ?? -1) >= 0, + groups: (settings.git_sync.include_type?.indexOf('group') ?? -1) >= 0 } } } else { @@ -508,7 +522,9 @@ resources: false, variables: false, secrets: false, - schedules: false + schedules: false, + users: false, + groups: false } } gitSyncTestJobs = [] @@ -1248,6 +1264,16 @@ on:change={(_) => resetGitSyncRepositoryExclude('resourcetypes')} options={{ right: 'Resource Types' }} /> + resetGitSyncRepositoryExclude('users')} + options={{ right: 'Users' }} + /> + resetGitSyncRepositoryExclude('groups')} + options={{ right: 'Groups' }} + /> @@ -1419,7 +1445,7 @@ gitSyncSettings.repositories = [ ...gitSyncSettings.repositories, { - script_path: 'hub/7958/sync-script-to-git-repo-windmill', + script_path: 'hub/8701/sync-script-to-git-repo-windmill', git_repo_resource_path: '', use_individual_branch: false, exclude_types_override: { @@ -1431,7 +1457,9 @@ resources: false, variables: false, secrets: false, - schedules: false + schedules: false, + users: false, + groups: false } } ]