From 14a1382d1c4b77ba16fee3ab867e6bfded85828e Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Tue, 3 Sep 2024 10:49:53 +0800 Subject: [PATCH] feat: call workspace delete when deleting user --- src/api/user.rs | 132 ++++--------------------------- src/api/workspace.rs | 9 ++- src/biz/user/mod.rs | 1 + src/biz/user/user_delete.rs | 152 ++++++++++++++++++++++++++++++++++++ src/biz/workspace/ops.rs | 8 +- 5 files changed, 180 insertions(+), 122 deletions(-) create mode 100644 src/biz/user/user_delete.rs diff --git a/src/api/user.rs b/src/api/user.rs index dbae51229..90871ab73 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,14 +1,12 @@ +use crate::biz::user::user_delete::delete_user; use crate::biz::user::user_info::{get_profile, get_user_workspace_info, update_user}; use crate::biz::user::user_verify::verify_token; use crate::state::AppState; use actix_web::web::{Data, Json}; use actix_web::Result; use actix_web::{web, Scope}; -use app_error::ErrorCode; use authentication::jwt::{Authorization, UserUuid}; use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo}; -use gotrue::params::AdminDeleteUserParams; -use secrecy::{ExposeSecret, Secret}; use shared_entity::dto::auth_dto::{DeleteUserQuery, SignInTokenResponse, UpdateUserParams}; use shared_entity::response::AppResponseError; use shared_entity::response::{AppResponse, JsonAppResponse}; @@ -73,117 +71,21 @@ async fn delete_user_handler( query: web::Query, ) -> Result, actix_web::Error> { let user_uuid = auth.uuid()?; - if is_apple_user(&auth) { - let query = query.into_inner(); - if let Err(err) = revoke_apple_user( - &state.config.apple_oauth.client_id, - &state.config.apple_oauth.client_secret, - query.provider_access_token, - query.provider_refresh_token, - ) - .await - { - tracing::warn!("revoke apple user failed: {:?}", err); - }; - } - - let admin_token = state.gotrue_admin.token().await?; - let _ = &state - .gotrue_client - .admin_delete_user( - &admin_token, - &user_uuid.to_string(), - &AdminDeleteUserParams { - should_soft_delete: false, - }, - ) - .await - .map_err(AppResponseError::from)?; - - Ok(AppResponse::Ok().into()) -} - -async fn revoke_apple_user( - client_id: &str, - client_secret: &Secret, - apple_access_token: Option, - apple_refresh_token: Option, -) -> Result<(), AppResponseError> { - let (type_type_hint, token) = match apple_access_token { - Some(access_token) => ("access_token", access_token), - None => match apple_refresh_token { - Some(refresh_token) => ("refresh_token", refresh_token), - None => { - return Err(AppResponseError::new( - ErrorCode::InvalidRequest, - "apple email deletion must provide access_token or refresh_token", - )) - }, - }, - }; - - if let Err(err) = revoke_apple_token_http_call( - client_id, - client_secret.expose_secret(), - &token, - type_type_hint, + let DeleteUserQuery { + provider_access_token, + provider_refresh_token, + } = query.into_inner(); + delete_user( + &state.pg_pool, + &state.bucket_storage, + &state.gotrue_client, + &state.gotrue_admin, + &state.config.apple_oauth, + auth, + user_uuid, + provider_access_token, + provider_refresh_token, ) - .await - { - tracing::warn!("revoke apple token failed: {:?}", err); - }; - Ok(()) -} - -fn is_apple_user(auth: &Authorization) -> bool { - if let Some(provider) = auth.claims.app_metadata.get("provider") { - if provider == "apple" { - return true; - } - }; - - if let Some(providers) = auth.claims.app_metadata.get("providers") { - if let Some(providers) = providers.as_array() { - for provider in providers { - if provider == "apple" { - return true; - } - } - } - } - - false -} - -/// Based on: https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens -async fn revoke_apple_token_http_call( - apple_client_id: &str, - apple_client_secret: &str, - apple_user_token: &str, - token_type_hint: &str, -) -> Result<(), AppResponseError> { - let resp = reqwest::Client::new() - .post("https://appleid.apple.com/auth/revoke") - .form(&[ - ("client_id", apple_client_id), - ("client_secret", apple_client_secret), - ("token", apple_user_token), - ("token_type_hint", token_type_hint), - ]) - .send() - .await?; - - let status = resp.status(); - if status.is_success() { - return Ok(()); - } - - let payload = resp.text().await?; - Err(AppResponseError::new( - ErrorCode::AppleRevokeTokenError, - format!( - "calling apple revoke, code: {}, message: {}", - status, payload - ), - )) + .await?; + Ok(AppResponse::Ok().into()) } diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 1aa5144ec..62cd5af88 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -250,10 +250,13 @@ async fn delete_workspace_handler( workspace_id: web::Path, state: Data, ) -> Result>> { - let bucket_storage = &state.bucket_storage; - // TODO: add permission for workspace deletion - workspace::ops::delete_workspace_for_user(&state.pg_pool, &workspace_id, bucket_storage).await?; + workspace::ops::delete_workspace_for_user( + state.pg_pool.clone(), + *workspace_id, + state.bucket_storage.clone(), + ) + .await?; Ok(AppResponse::Ok().into()) } diff --git a/src/biz/user/mod.rs b/src/biz/user/mod.rs index c1ad70566..b2523ff7f 100644 --- a/src/biz/user/mod.rs +++ b/src/biz/user/mod.rs @@ -1,3 +1,4 @@ +pub mod user_delete; pub mod user_info; pub mod user_init; pub mod user_verify; diff --git a/src/biz/user/user_delete.rs b/src/biz/user/user_delete.rs new file mode 100644 index 000000000..01bd6ec86 --- /dev/null +++ b/src/biz/user/user_delete.rs @@ -0,0 +1,152 @@ +use std::sync::Arc; + +use crate::biz::workspace::ops::get_all_user_workspaces; +use crate::state::GoTrueAdmin; +use crate::{biz::workspace::ops::delete_workspace_for_user, config::config::AppleOAuthSetting}; +use app_error::ErrorCode; +use authentication::jwt::Authorization; +use database::file::s3_client_impl::S3BucketStorage; +use gotrue::params::AdminDeleteUserParams; +use secrecy::{ExposeSecret, Secret}; +use shared_entity::response::AppResponseError; +use uuid::Uuid; + +#[allow(clippy::too_many_arguments)] +pub async fn delete_user( + pg_pool: &sqlx::PgPool, + bucket_storage: &Arc, + gotrue_client: &gotrue::api::Client, + gotrue_admin: &GoTrueAdmin, + apple_oauth: &AppleOAuthSetting, + auth: Authorization, + user_uuid: Uuid, + provider_access_token: Option, + provider_refresh_token: Option, +) -> Result<(), AppResponseError> { + if is_apple_user(&auth) { + if let Err(err) = revoke_apple_user( + &apple_oauth.client_id, + &apple_oauth.client_secret, + provider_access_token, + provider_refresh_token, + ) + .await + { + tracing::warn!("revoke apple user failed: {:?}", err); + }; + } + + let admin_token = gotrue_admin.token().await?; + gotrue_client + .admin_delete_user( + &admin_token, + &user_uuid.to_string(), + &AdminDeleteUserParams { + should_soft_delete: false, + }, + ) + .await + .map_err(AppResponseError::from)?; + + // spawn tasks to delete all user workspace and object storage + let user_workspaces = get_all_user_workspaces(pg_pool, &user_uuid, false).await?; + let mut tasks = vec![]; + for workspace in user_workspaces { + let cloned_pg_pool = pg_pool.clone(); + tasks.push(tokio::spawn(delete_workspace_for_user( + cloned_pg_pool, + workspace.workspace_id, + bucket_storage.clone(), + ))); + } + for task in tasks { + task.await??; + } + + Ok(()) +} + +async fn revoke_apple_user( + client_id: &str, + client_secret: &Secret, + apple_access_token: Option, + apple_refresh_token: Option, +) -> Result<(), AppResponseError> { + let (type_type_hint, token) = match apple_access_token { + Some(access_token) => ("access_token", access_token), + None => match apple_refresh_token { + Some(refresh_token) => ("refresh_token", refresh_token), + None => { + return Err(AppResponseError::new( + ErrorCode::InvalidRequest, + "apple email deletion must provide access_token or refresh_token", + )) + }, + }, + }; + + if let Err(err) = revoke_apple_token_http_call( + client_id, + client_secret.expose_secret(), + &token, + type_type_hint, + ) + .await + { + tracing::warn!("revoke apple token failed: {:?}", err); + }; + Ok(()) +} + +fn is_apple_user(auth: &Authorization) -> bool { + if let Some(provider) = auth.claims.app_metadata.get("provider") { + if provider == "apple" { + return true; + } + }; + + if let Some(providers) = auth.claims.app_metadata.get("providers") { + if let Some(providers) = providers.as_array() { + for provider in providers { + if provider == "apple" { + return true; + } + } + } + } + + false +} + +/// Based on: https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens +async fn revoke_apple_token_http_call( + apple_client_id: &str, + apple_client_secret: &str, + apple_user_token: &str, + token_type_hint: &str, +) -> Result<(), AppResponseError> { + let resp = reqwest::Client::new() + .post("https://appleid.apple.com/auth/revoke") + .form(&[ + ("client_id", apple_client_id), + ("client_secret", apple_client_secret), + ("token", apple_user_token), + ("token_type_hint", token_type_hint), + ]) + .send() + .await?; + + let status = resp.status(); + if status.is_success() { + return Ok(()); + } + + let payload = resp.text().await?; + Err(AppResponseError::new( + ErrorCode::AppleRevokeTokenError, + format!( + "calling apple revoke, code: {}, message: {}", + status, payload + ), + )) +} diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 3091781b0..2d841dd87 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -39,9 +39,9 @@ use crate::state::GoTrueAdmin; const MAX_COMMENT_LENGTH: usize = 5000; pub async fn delete_workspace_for_user( - pg_pool: &PgPool, - workspace_id: &Uuid, - bucket_storage: &Arc, + pg_pool: PgPool, + workspace_id: Uuid, + bucket_storage: Arc, ) -> Result<(), AppResponseError> { // remove files from s3 bucket_storage @@ -49,7 +49,7 @@ pub async fn delete_workspace_for_user( .await?; // remove from postgres - delete_from_workspace(pg_pool, workspace_id).await?; + delete_from_workspace(&pg_pool, &workspace_id).await?; // TODO: There can be a rare case where user uploads while workspace is being deleted. // We need some routine job to clean up these orphaned files.