From fd2a875fa19aa06efb4064039048ee15c243ab09 Mon Sep 17 00:00:00 2001 From: dzlk17 <105115971+dzlk17@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:23:25 +0100 Subject: [PATCH] Delete account grafana (#222) * delete_Account_grafana * delete_Account_grafana * delete_Account_grafana * create/delete user in grafana * add_member_to grafana team --- .../src/tables/domain_verifications/update.rs | 28 ++++- .../src/http/cloud/delete_account_finish.rs | 93 ++++++++++++-- server/src/http/cloud/delete_app.rs | 16 +-- server/src/http/cloud/delete_team.rs | 14 +-- .../cloud/grafana_utils/add_user_to_team.rs | 36 ++---- .../cloud/grafana_utils/create_new_team.rs | 76 +++++++++++- .../grafana_utils/delete_user_account.rs | 117 ++++++++++++++++++ server/src/http/cloud/grafana_utils/mod.rs | 1 + server/src/structs/cloud/api_cloud_errors.rs | 1 + 9 files changed, 324 insertions(+), 58 deletions(-) create mode 100644 server/src/http/cloud/grafana_utils/delete_user_account.rs diff --git a/database/src/tables/domain_verifications/update.rs b/database/src/tables/domain_verifications/update.rs index e8d26d05..c2938657 100644 --- a/database/src/tables/domain_verifications/update.rs +++ b/database/src/tables/domain_verifications/update.rs @@ -81,7 +81,6 @@ impl Db { domain_name: &String, app_id: &String, ) -> Result<(), DbError> { - let query_body = format!( "UPDATE {DOMAIN_VERIFICATIONS_TABLE_NAME} SET deleted_at = $1 WHERE domain_name = $2 AND app_id = $3 AND deleted_at IS NULL AND finished_at IS NOT NULL AND cancelled_at IS NULL" ); @@ -104,7 +103,6 @@ impl Db { tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, app_id: &String, ) -> Result<(), DbError> { - let query_body = format!( "UPDATE {DOMAIN_VERIFICATIONS_TABLE_NAME} SET deleted_at = $1 WHERE app_id = $2 AND deleted_at IS NULL" ); @@ -120,7 +118,33 @@ impl Db { Err(e) => Err(e).map_err(|e| e.into()), } } + pub async fn delete_domain_verifications_for_inactive_apps( + &self, + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + app_ids: &Vec, + ) -> Result<(), DbError> { + if app_ids.is_empty() { + return Ok(()); + } + + let query_body = format!( + "UPDATE {DOMAIN_VERIFICATIONS_TABLE_NAME} + SET deleted_at = $1 + WHERE app_id = ANY($2) + AND deleted_at IS NULL" + ); + let query_result = sqlx::query(&query_body) + .bind(&get_current_datetime()) + .bind(&app_ids) + .execute(&mut **tx) + .await; + + match query_result { + Ok(_) => Ok(()), + Err(e) => Err(e).map_err(|e| e.into()), + } + } } #[cfg(feature = "cloud_integration_tests")] diff --git a/server/src/http/cloud/delete_account_finish.rs b/server/src/http/cloud/delete_account_finish.rs index a6551253..276f76ab 100644 --- a/server/src/http/cloud/delete_account_finish.rs +++ b/server/src/http/cloud/delete_account_finish.rs @@ -1,4 +1,5 @@ use crate::{ + env::is_env_production, http::cloud::utils::{check_auth_code, validate_request}, middlewares::auth_middleware::UserId, structs::{ @@ -10,10 +11,13 @@ use axum::{extract::State, http::StatusCode, Extension, Json}; use database::db::Db; use garde::Validate; use log::error; +use openapi::apis::configuration::Configuration; use serde::{Deserialize, Serialize}; use std::sync::Arc; use ts_rs::TS; +use super::grafana_utils::delete_user_account::handle_grafana_delete_user_account; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] #[ts(export)] #[serde(rename_all = "camelCase")] @@ -25,6 +29,7 @@ pub struct HttpDeleteAccountFinishRequest { pub async fn delete_account_finish( State(db): State>, State(sessions_cache): State>, + State(grafana_conf): State>, Extension(user_id): Extension, Json(request): Json, ) -> Result, (StatusCode, String)> { @@ -84,11 +89,69 @@ pub async fn delete_account_finish( CloudApiErrors::DatabaseError.to_string(), ) })?; - + + let mut owned_team_grafana_ids = Vec::new(); + let mut non_owned_team_grafana_ids = Vec::new(); + let mut app_ids: Vec = Vec::new(); + + let teams = match db + .get_joined_teams_by_user_id(&user_id) + .await + .map_err(|err| { + error!("Failed to get user teams: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + ) + }) { + Ok(joined_teams) => joined_teams, + Err(err) => { + error!("Failed to get user teams: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + for (team, _, _, registered_apps) in teams { + if team.team_admin_id == user_id { + if let Some(grafana_id) = team.grafana_id { + owned_team_grafana_ids.push(grafana_id); + } + for (app, _) in registered_apps { + if app.team_id == team.team_id { + app_ids.push(app.app_id.clone()); + } + } + } else { + if let Some(grafana_id) = team.clone().grafana_id { + non_owned_team_grafana_ids.push(grafana_id) + } + } + } + // Grafana, delete teams, apps and user + if is_env_production() { + if let Err(err) = handle_grafana_delete_user_account( + &grafana_conf, + &owned_team_grafana_ids, + &non_owned_team_grafana_ids, + &user.email, + ) + .await + { + error!("Failed to delete account in grafana: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::GrafanaError.to_string(), + )); + }; + } + // Delete all invites connected to user if let Err(err) = db - .cancel_all_team_invites_containing_email(&mut tx, &user.email, &user_id) - .await + .cancel_all_team_invites_containing_email(&mut tx, &user.email, &user_id) + .await { error!("Failed to delete team invites: {:?}", err); return Err(( @@ -96,7 +159,19 @@ pub async fn delete_account_finish( CloudApiErrors::DatabaseError.to_string(), )); } - + + // Delete all verified domains + if let Err(err) = db + .delete_domain_verifications_for_inactive_apps(&mut tx, &app_ids) + .await + { + error!("Failed to delete domains: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + // Delete all user apps if let Err(err) = db.deactivate_user_apps(&mut tx, &user_id).await { error!("Failed to delete user apps: {:?}", err); @@ -105,7 +180,7 @@ pub async fn delete_account_finish( CloudApiErrors::DatabaseError.to_string(), )); } - + // Leave all teams if let Err(err) = db.remove_inactive_user_from_teams(&mut tx, &user_id).await { error!("Failed to leave teams: {:?}", err); @@ -117,9 +192,9 @@ pub async fn delete_account_finish( // delete privileges if let Err(err) = db - .remove_privileges_for_inactive_teams(&mut tx, &user_id) + .remove_privileges_for_inactive_teams(&mut tx, &user_id) .await - { + { error!("Failed to leave teams: {:?}", err); return Err(( StatusCode::INTERNAL_SERVER_ERROR, @@ -135,7 +210,7 @@ pub async fn delete_account_finish( CloudApiErrors::DatabaseError.to_string(), )); } - + // Deactivate the user if let Err(err) = db.deactivate_user(&user_id, &mut tx).await { error!("Failed to delete user: {:?}", err); @@ -144,7 +219,7 @@ pub async fn delete_account_finish( CloudApiErrors::DatabaseError.to_string(), )); } - + // Commit transaction tx.commit().await.map_err(|err| { error!("Failed to commit transaction: {:?}", err); diff --git a/server/src/http/cloud/delete_app.rs b/server/src/http/cloud/delete_app.rs index 8636b4b7..fd11daa1 100644 --- a/server/src/http/cloud/delete_app.rs +++ b/server/src/http/cloud/delete_app.rs @@ -72,19 +72,22 @@ pub async fn delete_app( // Start a transaction let mut tx = db.connection_pool.begin().await.unwrap(); - if let Err(err) = db.deactivate_app(&mut tx, &request.app_id).await { + if let Err(err) = db + .remove_privileges_for_inactive_app_within_tx(&mut tx, &request.app_id) + .await + { let _ = tx .rollback() .await .map_err(|err| error!("Failed to rollback transaction: {:?}", err)); - error!("Failed to deactivate app: {:?}", err); + error!("Failed to delete app: {:?}", err); return Err(( StatusCode::INTERNAL_SERVER_ERROR, CloudApiErrors::DatabaseError.to_string(), )); } if let Err(err) = db - .remove_privileges_for_inactive_app_within_tx(&mut tx, &request.app_id) + .delete_domain_verification_for_inactive_app(&mut tx, &request.app_id) .await { let _ = tx @@ -97,15 +100,12 @@ pub async fn delete_app( CloudApiErrors::DatabaseError.to_string(), )); } - if let Err(err) = db - .delete_domain_verification_for_inactive_app(&mut tx, &request.app_id) - .await - { + if let Err(err) = db.deactivate_app(&mut tx, &request.app_id).await { let _ = tx .rollback() .await .map_err(|err| error!("Failed to rollback transaction: {:?}", err)); - error!("Failed to delete app: {:?}", err); + error!("Failed to deactivate app: {:?}", err); return Err(( StatusCode::INTERNAL_SERVER_ERROR, CloudApiErrors::DatabaseError.to_string(), diff --git a/server/src/http/cloud/delete_team.rs b/server/src/http/cloud/delete_team.rs index 752a9a03..0e9904c9 100644 --- a/server/src/http/cloud/delete_team.rs +++ b/server/src/http/cloud/delete_team.rs @@ -109,12 +109,15 @@ pub async fn delete_team( // Delete team apps, privileges and domain verifications for app in registered_apps.iter() { - if let Err(err) = db.deactivate_app(&mut tx, &app.app_id).await { + if let Err(err) = db + .delete_domain_verification_for_inactive_app(&mut tx, &app.app_id) + .await + { let _ = tx .rollback() .await .map_err(|err| error!("Failed to rollback transaction: {:?}", err)); - error!("Failed to deactivate app: {:?}", err); + error!("Failed to create app: {:?}", err); return Err(( StatusCode::INTERNAL_SERVER_ERROR, CloudApiErrors::DatabaseError.to_string(), @@ -134,15 +137,12 @@ pub async fn delete_team( CloudApiErrors::DatabaseError.to_string(), )); } - if let Err(err) = db - .delete_domain_verification_for_inactive_app(&mut tx, &app.app_id) - .await - { + if let Err(err) = db.deactivate_app(&mut tx, &app.app_id).await { let _ = tx .rollback() .await .map_err(|err| error!("Failed to rollback transaction: {:?}", err)); - error!("Failed to create app: {:?}", err); + error!("Failed to deactivate app: {:?}", err); return Err(( StatusCode::INTERNAL_SERVER_ERROR, CloudApiErrors::DatabaseError.to_string(), diff --git a/server/src/http/cloud/grafana_utils/add_user_to_team.rs b/server/src/http/cloud/grafana_utils/add_user_to_team.rs index 801fb8a2..86085667 100644 --- a/server/src/http/cloud/grafana_utils/add_user_to_team.rs +++ b/server/src/http/cloud/grafana_utils/add_user_to_team.rs @@ -5,14 +5,12 @@ use axum::http::StatusCode; use log::{error, warn}; use openapi::{ apis::{ - admin_users_api::admin_create_user, configuration::Configuration, teams_api::add_team_member, users_api::{get_user_by_login_or_email, get_user_teams}, }, - models::{AddTeamMemberCommand, AdminCreateUserForm}, + models::AddTeamMemberCommand, }; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; use std::sync::Arc; pub async fn handle_grafana_add_user_to_team( @@ -23,32 +21,12 @@ pub async fn handle_grafana_add_user_to_team( // Check if user exists, if not create a new user let user_id = match get_user_by_login_or_email(&grafana_conf, user_email).await { Ok(user) => user.id, - Err(_) => { - // Create user with the same email as the user, password can be anything, it won't be used - let random_password: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(30) - .map(char::from) - .collect(); - - let request = AdminCreateUserForm { - password: Some(random_password), - email: Some(user_email.to_lowercase().clone()), - login: None, - name: None, - org_id: None, - }; - - match admin_create_user(&grafana_conf, request).await { - Ok(user) => user.id, - Err(err) => { - warn!("Failed to create user: {:?}", err); - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - CloudApiErrors::InternalServerError.to_string(), - )); - } - } + Err(err) => { + warn!("Failed to get user from grafana: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); } }; diff --git a/server/src/http/cloud/grafana_utils/create_new_team.rs b/server/src/http/cloud/grafana_utils/create_new_team.rs index 21b8d0d6..1c50d5ff 100644 --- a/server/src/http/cloud/grafana_utils/create_new_team.rs +++ b/server/src/http/cloud/grafana_utils/create_new_team.rs @@ -5,13 +5,19 @@ use axum::http::StatusCode; use log::warn; use openapi::{ apis::{ - configuration::Configuration, folder_permissions_api::update_folder_permissions, - folders_api::create_folder, teams_api::create_team, + admin_users_api::admin_create_user, + configuration::Configuration, + folder_permissions_api::update_folder_permissions, + folders_api::create_folder, + teams_api::{add_team_member, create_team}, + users_api::get_user_by_login_or_email, }, models::{ - CreateFolderCommand, CreateTeamCommand, DashboardAclUpdateItem, UpdateDashboardAclCommand, + AddTeamMemberCommand, AdminCreateUserForm, CreateFolderCommand, CreateTeamCommand, + DashboardAclUpdateItem, UpdateDashboardAclCommand, }, }; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; use std::sync::Arc; pub async fn handle_grafana_create_new_team( @@ -20,6 +26,55 @@ pub async fn handle_grafana_create_new_team( team_name: &String, ) -> Result { let grafana_team_name = format!("[{}][{}]", team_name, admin_email); + // Check if user exists, if not create a new user + let admin_id = match get_user_by_login_or_email(&grafana_conf, admin_email).await { + Ok(user) => match user.id { + Some(id) => id, + None => { + warn!("Failed to get user from grafana: {:?}", user); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }, + Err(_) => { + // Create user with the same email as the user, password can be anything, it won't be used + let random_password: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect(); + + let request = AdminCreateUserForm { + password: Some(random_password), + email: Some(admin_email.to_lowercase().clone()), + login: None, + name: None, + org_id: None, + }; + + match admin_create_user(&grafana_conf, request).await { + Ok(create_user_response) => match create_user_response.id { + Some(id) => id, + None => { + warn!("Failed to create user: {:?}", create_user_response); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }, + Err(err) => { + warn!("Failed to create user: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + } + } + }; // create new team let team_request = CreateTeamCommand { @@ -83,5 +138,20 @@ pub async fn handle_grafana_create_new_team( return Err(handle_grafana_error(err)); } + // Add user to the team + let request = AddTeamMemberCommand { + user_id: Some(admin_id), + }; + + if let Err(err) = + add_team_member(&grafana_conf, grafana_team_id.to_string().as_str(), request).await + { + warn!( + "Failed to add user [{:?}] to team [{:?}], error: {:?}", + admin_email, grafana_team_id, err + ); + return Err(handle_grafana_error(err)); + } + Ok(grafana_team_id) } diff --git a/server/src/http/cloud/grafana_utils/delete_user_account.rs b/server/src/http/cloud/grafana_utils/delete_user_account.rs new file mode 100644 index 00000000..bf5d1f86 --- /dev/null +++ b/server/src/http/cloud/grafana_utils/delete_user_account.rs @@ -0,0 +1,117 @@ +use crate::structs::cloud::{ + api_cloud_errors::CloudApiErrors, grafana_error::handle_grafana_error, +}; +use axum::http::StatusCode; +use log::warn; +use openapi::apis::{ + admin_users_api::admin_delete_user, + configuration::Configuration, + folders_api::delete_folder, + teams_api::{delete_team_by_id, get_team_by_id, remove_team_member}, + users_api::get_user_by_login_or_email, +}; +use std::sync::Arc; + +pub async fn handle_grafana_delete_user_account( + grafana_conf: &Arc, + owned_team_grafana_ids: &Vec, + non_owned_team_grafana_ids: &Vec, + user_email: &String, +) -> Result<(), (StatusCode, String)> { + for team_id in owned_team_grafana_ids { + match get_team_by_id(&grafana_conf, team_id).await { + Ok(response) => match response.id { + Some(_) => (), + None => { + warn!("Failed to get team: {:?}, team_id: {:?}", response, team_id); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::TeamDoesNotExist.to_string(), + )); + } + }, + Err(err) => { + warn!("Failed to get team: {:?}, team_id: {:?}", err, team_id); + return Err(handle_grafana_error(err)); + } + }; + + match delete_team_by_id(&grafana_conf, team_id).await { + Ok(_) => (), + Err(err) => { + warn!("Failed to delete team: {:?}, team_id: {:?}", err, team_id); + return Err(handle_grafana_error(err)); + } + } + + if let Err(err) = delete_folder(&grafana_conf, team_id, Some(false)).await { + warn!("Failed to delete folder: {:?}, team_id: {:?}", err, team_id); + return Err(handle_grafana_error(err)); + }; + } + // Check if user exists, if not return error + let user_id = match get_user_by_login_or_email( + &grafana_conf, + user_email.as_str().to_lowercase().as_str(), + ) + .await + { + Ok(user) => match user.id { + Some(id) => id, + None => { + warn!("Failed to get id for user: {:?}", user); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::UserDoesNotExistInGrafana.to_string(), + )); + } + }, + Err(_) => { + warn!("Failed to get user: {:?}", user_email); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::UserDoesNotExistInGrafana.to_string(), + )); + } + }; + + for team_id in non_owned_team_grafana_ids { + match get_team_by_id(&grafana_conf, team_id).await { + Ok(response) => match response.id { + Some(_) => (), + None => { + warn!("Failed to get team: {:?}, team_id: {:?}", response, team_id); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::TeamDoesNotExist.to_string(), + )); + } + }, + Err(err) => { + warn!("Failed to get team: {:?}, team_id: {:?}", err, team_id); + return Err(handle_grafana_error(err)); + } + }; + + match remove_team_member(&grafana_conf, team_id, user_id).await { + Ok(_) => (), + Err(err) => { + warn!( + "Failed to remove user from team: {:?}, team_id: {:?}", + err, team_id + ); + return Err(handle_grafana_error(err)); + } + } + } + + match admin_delete_user(&grafana_conf, user_id).await { + Ok(_) => (), + Err(err) => { + warn!("Failed to delete user: {:?}", err); + return Err(handle_grafana_error(err)); + } + } + + Ok(()) +} diff --git a/server/src/http/cloud/grafana_utils/mod.rs b/server/src/http/cloud/grafana_utils/mod.rs index e1af8f68..90367e8e 100644 --- a/server/src/http/cloud/grafana_utils/mod.rs +++ b/server/src/http/cloud/grafana_utils/mod.rs @@ -3,6 +3,7 @@ pub mod create_new_app; pub mod create_new_team; pub mod delete_registered_app; pub mod delete_team; +pub mod delete_user_account; pub mod import_template_dashboard; pub mod remove_user_from_the_team; pub mod setup_database_datasource; diff --git a/server/src/structs/cloud/api_cloud_errors.rs b/server/src/structs/cloud/api_cloud_errors.rs index 0689b0f8..5ae4dc1d 100644 --- a/server/src/structs/cloud/api_cloud_errors.rs +++ b/server/src/structs/cloud/api_cloud_errors.rs @@ -55,4 +55,5 @@ pub enum CloudApiErrors { AdminCannotLeaveTeam, GrafanaError, TeamWithoutGrafanaId, + UserDoesNotExistInGrafana, }