From 8337eaefdd86d35969c6e5c874350b88f501d8e5 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 7 Apr 2022 20:52:17 +0000 Subject: [PATCH] Federate user account deletion (fixes #1284) (#2199) --- api_tests/src/shared.ts | 11 +++ api_tests/src/user.spec.ts | 39 ++++++++++ crates/api/src/site.rs | 8 +- crates/api_common/src/lib.rs | 26 ++++++- crates/api_crud/src/site/read.rs | 8 +- crates/api_crud/src/user/delete.rs | 23 +----- .../activities/deletion/delete_user.json | 12 +++ .../src/activities/deletion/delete_user.rs | 74 +++++++++++++++++++ crates/apub/src/activities/deletion/mod.rs | 1 + crates/apub/src/activity_lists.rs | 4 +- .../activities/deletion/delete_user.rs | 24 ++++++ .../src/protocol/activities/deletion/mod.rs | 6 +- 12 files changed, 200 insertions(+), 36 deletions(-) create mode 100644 crates/apub/assets/lemmy/activities/deletion/delete_user.json create mode 100644 crates/apub/src/activities/deletion/delete_user.rs create mode 100644 crates/apub/src/protocol/activities/deletion/delete_user.rs diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 4f305237e..c429a207a 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -58,6 +58,7 @@ import { CommentReportResponse, ListCommentReports, ListCommentReportsResponse, + DeleteAccount, } from 'lemmy-js-client'; export interface API { @@ -549,6 +550,16 @@ export async function saveUserSettings( return api.client.saveUserSettings(form); } +export async function deleteUser( + api: API, +): Promise { + let form: DeleteAccount = { + auth: api.auth, + password + }; + return api.client.deleteAccount(form); +} + export async function getSite( api: API ): Promise { diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index 788987e2c..29029ac1e 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -6,6 +6,15 @@ import { resolvePerson, saveUserSettings, getSite, + createPost, + gamma, + resolveCommunity, + createComment, + resolveBetaCommunity, + deleteUser, + resolvePost, + API, + resolveComment, } from './shared'; import { PersonViewSafe, @@ -60,3 +69,33 @@ test('Set some user settings, check that they are federated', async () => { let betaPerson = (await resolvePerson(beta, apShortname)).person; assertUserFederation(alphaPerson, betaPerson); }); + +test('Delete user', async () => { + let userRes = await registerUser(alpha); + expect(userRes.jwt).toBeDefined(); + let user: API = { + client: alpha.client, + auth: userRes.jwt + } + + // make a local post and comment + let alphaCommunity = (await resolveCommunity(user, '!main@lemmy-alpha:8541')).community; + let localPost = (await createPost(user, alphaCommunity.community.id)).post_view.post; + expect(localPost).toBeDefined(); + let localComment = (await createComment(user, localPost.id)).comment_view.comment; + expect(localComment).toBeDefined(); + + // make a remote post and comment + let betaCommunity = (await resolveBetaCommunity(user)).community; + let remotePost = (await createPost(user, betaCommunity.community.id)).post_view.post; + expect(remotePost).toBeDefined(); + let remoteComment = (await createComment(user, remotePost.id)).comment_view.comment; + expect(remoteComment).toBeDefined(); + + await deleteUser(user); + + expect((await resolvePost(alpha, localPost)).post).toBeUndefined(); + expect((await resolveComment(alpha, localComment)).comment).toBeUndefined(); + expect((await resolvePost(alpha, remotePost)).post).toBeUndefined(); + expect((await resolveComment(alpha, remoteComment)).comment).toBeUndefined(); +}); diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 677576e13..54b7e0c73 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -508,12 +508,8 @@ impl Perform for LeaveAdmin { let site_view = blocking(context.pool(), SiteView::read_local).await??; let admins = blocking(context.pool(), PersonViewSafe::admins).await??; - let federated_instances = build_federated_instances( - context.pool(), - &context.settings().federation, - &context.settings().hostname, - ) - .await?; + let federated_instances = + build_federated_instances(context.pool(), &context.settings()).await?; Ok(GetSiteResponse { site_view: Some(site_view), diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 0d6789e7b..c37d40e9a 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -13,6 +13,7 @@ use lemmy_db_schema::{ community::Community, email_verification::{EmailVerification, EmailVerificationForm}, password_reset_request::PasswordResetRequest, + person::Person, person_block::PersonBlock, post::{Post, PostRead, PostReadForm}, registration_application::RegistrationApplication, @@ -34,7 +35,7 @@ use lemmy_db_views_actor::{ use lemmy_utils::{ claims::Claims, email::{send_email, translations::Lang}, - settings::structs::{FederationConfig, Settings}, + settings::structs::Settings, utils::generate_random_string, LemmyError, Sensitive, @@ -295,9 +296,10 @@ pub async fn check_private_instance( #[tracing::instrument(skip_all)] pub async fn build_federated_instances( pool: &DbPool, - federation_config: &FederationConfig, - hostname: &str, + settings: &Settings, ) -> Result, LemmyError> { + let federation_config = &settings.federation; + let hostname = &settings.hostname; let federation = federation_config.to_owned(); if federation.enabled { let distinct_communities = blocking(pool, move |conn| { @@ -579,6 +581,24 @@ pub async fn remove_user_data_in_community( Ok(()) } +pub async fn delete_user_account(person_id: PersonId, pool: &DbPool) -> Result<(), LemmyError> { + // Comments + let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id); + blocking(pool, permadelete) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; + + // Posts + let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id); + blocking(pool, permadelete) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?; + + blocking(pool, move |conn| Person::delete_account(conn, person_id)).await??; + + Ok(()) +} + pub fn check_image_has_local_domain(url: &Option) -> Result<(), LemmyError> { if let Some(url) = url { let settings = Settings::get(); diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 2e8269de8..9131c3ecf 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -134,12 +134,8 @@ impl PerformCrud for GetSite { None }; - let federated_instances = build_federated_instances( - context.pool(), - &context.settings().federation, - &context.settings().hostname, - ) - .await?; + let federated_instances = + build_federated_instances(context.pool(), &context.settings()).await?; Ok(GetSiteResponse { site_view, diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index ae92c12e3..ea1cbcff5 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -1,8 +1,8 @@ use crate::PerformCrud; use actix_web::web::Data; use bcrypt::verify; -use lemmy_api_common::{blocking, get_local_user_view_from_jwt, person::*}; -use lemmy_db_schema::source::{comment::Comment, person::Person, post::Post}; +use lemmy_api_common::{delete_user_account, get_local_user_view_from_jwt, person::*}; +use lemmy_apub::protocol::activities::deletion::delete_user::DeleteUser; use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_websocket::LemmyContext; @@ -30,23 +30,8 @@ impl PerformCrud for DeleteAccount { return Err(LemmyError::from_message("password_incorrect")); } - // Comments - let person_id = local_user_view.person.id; - let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id); - blocking(context.pool(), permadelete) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; - - // Posts - let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id); - blocking(context.pool(), permadelete) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_post"))?; - - blocking(context.pool(), move |conn| { - Person::delete_account(conn, person_id) - }) - .await??; + delete_user_account(local_user_view.person.id, context.pool()).await?; + DeleteUser::send(&local_user_view.person.into(), context).await?; Ok(DeleteAccountResponse {}) } diff --git a/crates/apub/assets/lemmy/activities/deletion/delete_user.json b/crates/apub/assets/lemmy/activities/deletion/delete_user.json new file mode 100644 index 000000000..1e000c12d --- /dev/null +++ b/crates/apub/assets/lemmy/activities/deletion/delete_user.json @@ -0,0 +1,12 @@ +{ + "actor": "http://ds9.lemmy.ml/u/lemmy_alpha", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "http://ds9.lemmy.ml/u/lemmy_alpha", + "cc": [ + "http://enterprise.lemmy.ml/c/main" + ], + "type": "Delete", + "id": "http://ds9.lemmy.ml/activities/delete/f2abee48-c7bb-41d5-9e27-8775ff32db12" +} \ No newline at end of file diff --git a/crates/apub/src/activities/deletion/delete_user.rs b/crates/apub/src/activities/deletion/delete_user.rs new file mode 100644 index 000000000..0cd2c5010 --- /dev/null +++ b/crates/apub/src/activities/deletion/delete_user.rs @@ -0,0 +1,74 @@ +use crate::{ + activities::{generate_activity_id, send_lemmy_activity, verify_is_public, verify_person}, + objects::person::ApubPerson, + protocol::activities::deletion::delete_user::DeleteUser, +}; +use activitystreams_kinds::{activity::DeleteType, public}; +use lemmy_api_common::{blocking, delete_user_account}; +use lemmy_apub_lib::{ + data::Data, + object_id::ObjectId, + traits::ActivityHandler, + verify::verify_urls_match, +}; +use lemmy_db_schema::source::site::Site; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; + +/// This can be separate from Delete activity because it doesn't need to be handled in shared inbox +/// (cause instance actor doesn't have shared inbox). +#[async_trait::async_trait(?Send)] +impl ActivityHandler for DeleteUser { + type DataType = LemmyContext; + + async fn verify( + &self, + context: &Data, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + verify_is_public(&self.to, &[])?; + verify_person(&self.actor, context, request_counter).await?; + verify_urls_match(self.actor.inner(), self.object.inner())?; + Ok(()) + } + + async fn receive( + self, + context: &Data, + request_counter: &mut i32, + ) -> Result<(), LemmyError> { + let actor = self + .actor + .dereference(context, context.client(), request_counter) + .await?; + delete_user_account(actor.id, context.pool()).await?; + Ok(()) + } +} + +impl DeleteUser { + #[tracing::instrument(skip_all)] + pub async fn send(actor: &ApubPerson, context: &LemmyContext) -> Result<(), LemmyError> { + let actor_id = ObjectId::new(actor.actor_id.clone()); + let id = generate_activity_id( + DeleteType::Delete, + &context.settings().get_protocol_and_hostname(), + )?; + let delete = DeleteUser { + actor: actor_id.clone(), + to: vec![public()], + object: actor_id, + kind: DeleteType::Delete, + id: id.clone(), + cc: vec![], + }; + + let remote_sites = blocking(context.pool(), Site::read_remote_sites).await??; + let inboxes = remote_sites + .into_iter() + .map(|s| s.inbox_url.into()) + .collect(); + send_lemmy_activity(context, &delete, &id, actor, inboxes, true).await?; + Ok(()) + } +} diff --git a/crates/apub/src/activities/deletion/mod.rs b/crates/apub/src/activities/deletion/mod.rs index f0c3a541f..1ff8429aa 100644 --- a/crates/apub/src/activities/deletion/mod.rs +++ b/crates/apub/src/activities/deletion/mod.rs @@ -49,6 +49,7 @@ use std::ops::Deref; use url::Url; pub mod delete; +pub mod delete_user; pub mod undo_delete; /// Parameter `reason` being set indicates that this is a removal by a mod. If its unset, this diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs index 362d29afb..80d37fc63 100644 --- a/crates/apub/src/activity_lists.rs +++ b/crates/apub/src/activity_lists.rs @@ -16,7 +16,7 @@ use crate::{ post::CreateOrUpdatePost, private_message::CreateOrUpdatePrivateMessage, }, - deletion::{delete::Delete, undo_delete::UndoDelete}, + deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete}, following::{ accept::AcceptFollowCommunity, follow::FollowCommunity, @@ -87,9 +87,11 @@ pub enum AnnouncableActivities { #[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] #[serde(untagged)] #[activity_handler(LemmyContext)] +#[allow(clippy::enum_variant_names)] pub enum SiteInboxActivities { BlockUser(BlockUser), UndoBlockUser(UndoBlockUser), + DeleteUser(DeleteUser), } #[async_trait::async_trait(?Send)] diff --git a/crates/apub/src/protocol/activities/deletion/delete_user.rs b/crates/apub/src/protocol/activities/deletion/delete_user.rs new file mode 100644 index 000000000..22d215eb4 --- /dev/null +++ b/crates/apub/src/protocol/activities/deletion/delete_user.rs @@ -0,0 +1,24 @@ +use crate::objects::person::ApubPerson; +use activitystreams_kinds::activity::DeleteType; +use lemmy_apub_lib::object_id::ObjectId; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use url::Url; + +#[skip_serializing_none] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteUser { + pub(crate) actor: ObjectId, + #[serde(deserialize_with = "crate::deserialize_one_or_many")] + pub(crate) to: Vec, + pub(crate) object: ObjectId, + #[serde(rename = "type")] + pub(crate) kind: DeleteType, + pub(crate) id: Url, + + #[serde(deserialize_with = "crate::deserialize_one_or_many")] + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub(crate) cc: Vec, +} diff --git a/crates/apub/src/protocol/activities/deletion/mod.rs b/crates/apub/src/protocol/activities/deletion/mod.rs index 24f7ab16e..fe22c0010 100644 --- a/crates/apub/src/protocol/activities/deletion/mod.rs +++ b/crates/apub/src/protocol/activities/deletion/mod.rs @@ -1,10 +1,11 @@ pub mod delete; +pub mod delete_user; pub mod undo_delete; #[cfg(test)] mod tests { use crate::protocol::{ - activities::deletion::{delete::Delete, undo_delete::UndoDelete}, + activities::deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete}, tests::test_parse_lemmy_item, }; @@ -23,5 +24,8 @@ mod tests { "assets/lemmy/activities/deletion/undo_delete_private_message.json", ) .unwrap(); + + test_parse_lemmy_item::("assets/lemmy/activities/deletion/delete_user.json") + .unwrap(); } }