Federate user account deletion (fixes #1284) (#2199)

This commit is contained in:
Nutomic 2022-04-07 20:52:17 +00:00 committed by GitHub
parent 9ac1f46a2b
commit 8337eaefdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 36 deletions

View file

@ -58,6 +58,7 @@ import {
CommentReportResponse, CommentReportResponse,
ListCommentReports, ListCommentReports,
ListCommentReportsResponse, ListCommentReportsResponse,
DeleteAccount,
} from 'lemmy-js-client'; } from 'lemmy-js-client';
export interface API { export interface API {
@ -549,6 +550,16 @@ export async function saveUserSettings(
return api.client.saveUserSettings(form); return api.client.saveUserSettings(form);
} }
export async function deleteUser(
api: API,
): Promise<LoginResponse> {
let form: DeleteAccount = {
auth: api.auth,
password
};
return api.client.deleteAccount(form);
}
export async function getSite( export async function getSite(
api: API api: API
): Promise<GetSiteResponse> { ): Promise<GetSiteResponse> {

View file

@ -6,6 +6,15 @@ import {
resolvePerson, resolvePerson,
saveUserSettings, saveUserSettings,
getSite, getSite,
createPost,
gamma,
resolveCommunity,
createComment,
resolveBetaCommunity,
deleteUser,
resolvePost,
API,
resolveComment,
} from './shared'; } from './shared';
import { import {
PersonViewSafe, PersonViewSafe,
@ -60,3 +69,33 @@ test('Set some user settings, check that they are federated', async () => {
let betaPerson = (await resolvePerson(beta, apShortname)).person; let betaPerson = (await resolvePerson(beta, apShortname)).person;
assertUserFederation(alphaPerson, betaPerson); 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();
});

View file

@ -508,12 +508,8 @@ impl Perform for LeaveAdmin {
let site_view = blocking(context.pool(), SiteView::read_local).await??; let site_view = blocking(context.pool(), SiteView::read_local).await??;
let admins = blocking(context.pool(), PersonViewSafe::admins).await??; let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
let federated_instances = build_federated_instances( let federated_instances =
context.pool(), build_federated_instances(context.pool(), &context.settings()).await?;
&context.settings().federation,
&context.settings().hostname,
)
.await?;
Ok(GetSiteResponse { Ok(GetSiteResponse {
site_view: Some(site_view), site_view: Some(site_view),

View file

@ -13,6 +13,7 @@ use lemmy_db_schema::{
community::Community, community::Community,
email_verification::{EmailVerification, EmailVerificationForm}, email_verification::{EmailVerification, EmailVerificationForm},
password_reset_request::PasswordResetRequest, password_reset_request::PasswordResetRequest,
person::Person,
person_block::PersonBlock, person_block::PersonBlock,
post::{Post, PostRead, PostReadForm}, post::{Post, PostRead, PostReadForm},
registration_application::RegistrationApplication, registration_application::RegistrationApplication,
@ -34,7 +35,7 @@ use lemmy_db_views_actor::{
use lemmy_utils::{ use lemmy_utils::{
claims::Claims, claims::Claims,
email::{send_email, translations::Lang}, email::{send_email, translations::Lang},
settings::structs::{FederationConfig, Settings}, settings::structs::Settings,
utils::generate_random_string, utils::generate_random_string,
LemmyError, LemmyError,
Sensitive, Sensitive,
@ -295,9 +296,10 @@ pub async fn check_private_instance(
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn build_federated_instances( pub async fn build_federated_instances(
pool: &DbPool, pool: &DbPool,
federation_config: &FederationConfig, settings: &Settings,
hostname: &str,
) -> Result<Option<FederatedInstances>, LemmyError> { ) -> Result<Option<FederatedInstances>, LemmyError> {
let federation_config = &settings.federation;
let hostname = &settings.hostname;
let federation = federation_config.to_owned(); let federation = federation_config.to_owned();
if federation.enabled { if federation.enabled {
let distinct_communities = blocking(pool, move |conn| { let distinct_communities = blocking(pool, move |conn| {
@ -579,6 +581,24 @@ pub async fn remove_user_data_in_community(
Ok(()) 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<DbUrl>) -> Result<(), LemmyError> { pub fn check_image_has_local_domain(url: &Option<DbUrl>) -> Result<(), LemmyError> {
if let Some(url) = url { if let Some(url) = url {
let settings = Settings::get(); let settings = Settings::get();

View file

@ -134,12 +134,8 @@ impl PerformCrud for GetSite {
None None
}; };
let federated_instances = build_federated_instances( let federated_instances =
context.pool(), build_federated_instances(context.pool(), &context.settings()).await?;
&context.settings().federation,
&context.settings().hostname,
)
.await?;
Ok(GetSiteResponse { Ok(GetSiteResponse {
site_view, site_view,

View file

@ -1,8 +1,8 @@
use crate::PerformCrud; use crate::PerformCrud;
use actix_web::web::Data; use actix_web::web::Data;
use bcrypt::verify; use bcrypt::verify;
use lemmy_api_common::{blocking, get_local_user_view_from_jwt, person::*}; use lemmy_api_common::{delete_user_account, get_local_user_view_from_jwt, person::*};
use lemmy_db_schema::source::{comment::Comment, person::Person, post::Post}; use lemmy_apub::protocol::activities::deletion::delete_user::DeleteUser;
use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_utils::{ConnectionId, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -30,23 +30,8 @@ impl PerformCrud for DeleteAccount {
return Err(LemmyError::from_message("password_incorrect")); return Err(LemmyError::from_message("password_incorrect"));
} }
// Comments delete_user_account(local_user_view.person.id, context.pool()).await?;
let person_id = local_user_view.person.id; DeleteUser::send(&local_user_view.person.into(), context).await?;
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??;
Ok(DeleteAccountResponse {}) Ok(DeleteAccountResponse {})
} }

View file

@ -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"
}

View file

@ -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<LemmyContext>,
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<LemmyContext>,
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(())
}
}

View file

@ -49,6 +49,7 @@ use std::ops::Deref;
use url::Url; use url::Url;
pub mod delete; pub mod delete;
pub mod delete_user;
pub mod undo_delete; pub mod undo_delete;
/// Parameter `reason` being set indicates that this is a removal by a mod. If its unset, this /// Parameter `reason` being set indicates that this is a removal by a mod. If its unset, this

View file

@ -16,7 +16,7 @@ use crate::{
post::CreateOrUpdatePost, post::CreateOrUpdatePost,
private_message::CreateOrUpdatePrivateMessage, private_message::CreateOrUpdatePrivateMessage,
}, },
deletion::{delete::Delete, undo_delete::UndoDelete}, deletion::{delete::Delete, delete_user::DeleteUser, undo_delete::UndoDelete},
following::{ following::{
accept::AcceptFollowCommunity, accept::AcceptFollowCommunity,
follow::FollowCommunity, follow::FollowCommunity,
@ -87,9 +87,11 @@ pub enum AnnouncableActivities {
#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)] #[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
#[serde(untagged)] #[serde(untagged)]
#[activity_handler(LemmyContext)] #[activity_handler(LemmyContext)]
#[allow(clippy::enum_variant_names)]
pub enum SiteInboxActivities { pub enum SiteInboxActivities {
BlockUser(BlockUser), BlockUser(BlockUser),
UndoBlockUser(UndoBlockUser), UndoBlockUser(UndoBlockUser),
DeleteUser(DeleteUser),
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]

View file

@ -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<ApubPerson>,
#[serde(deserialize_with = "crate::deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: ObjectId<ApubPerson>,
#[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<Url>,
}

View file

@ -1,10 +1,11 @@
pub mod delete; pub mod delete;
pub mod delete_user;
pub mod undo_delete; pub mod undo_delete;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::protocol::{ 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, tests::test_parse_lemmy_item,
}; };
@ -23,5 +24,8 @@ mod tests {
"assets/lemmy/activities/deletion/undo_delete_private_message.json", "assets/lemmy/activities/deletion/undo_delete_private_message.json",
) )
.unwrap(); .unwrap();
test_parse_lemmy_item::<DeleteUser>("assets/lemmy/activities/deletion/delete_user.json")
.unwrap();
} }
} }