Allow admins to resolve removed or deleted objects via API (#5061)

* Allow admins to resolve removed or deleted objects via API

* Removing pointless TestUser.

---------

Co-authored-by: Dessalines <tyhou13@gmx.com>
This commit is contained in:
Richard Schwab 2024-10-26 20:47:56 +02:00 committed by GitHub
parent 925826170f
commit 920ffe1803
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 185 additions and 71 deletions

View file

@ -158,16 +158,16 @@ test("Delete a comment", async () => {
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
expect(deleteCommentRes.comment_view.comment.content).toBe(""); expect(deleteCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is undefined on beta // Make sure that comment is deleted on beta
await waitUntil( await waitUntil(
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), () => resolveComment(beta, commentRes.comment_view.comment),
e => e.message == "not_found", c => c.comment?.comment.deleted === true,
); );
// Make sure that comment is undefined on gamma after delete // Make sure that comment is deleted on gamma after delete
await waitUntil( await waitUntil(
() => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e), () => resolveComment(gamma, commentRes.comment_view.comment),
e => e.message === "not_found", c => c.comment?.comment.deleted === true,
); );
// Test undeleting the comment // Test undeleting the comment
@ -181,11 +181,10 @@ test("Delete a comment", async () => {
// Make sure that comment is undeleted on beta // Make sure that comment is undeleted on beta
let betaComment2 = ( let betaComment2 = (
await waitUntil( await waitUntil(
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e), () => resolveComment(beta, commentRes.comment_view.comment),
e => e.message !== "not_found", c => c.comment?.comment.deleted === false,
) )
).comment; ).comment;
expect(betaComment2?.comment.deleted).toBe(false);
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view); assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
}); });

View file

@ -5,7 +5,6 @@ use crate::fetcher::{
}; };
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::{Json, Query}; use actix_web::web::{Json, Query};
use diesel::NotFound;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{ResolveObject, ResolveObjectResponse}, site::{ResolveObject, ResolveObjectResponse},
@ -47,36 +46,145 @@ async fn convert_response(
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<Json<ResolveObjectResponse>> { ) -> LemmyResult<Json<ResolveObjectResponse>> {
let removed_or_deleted;
let mut res = ResolveObjectResponse::default(); let mut res = ResolveObjectResponse::default();
let local_user = local_user_view.map(|l| l.local_user); let local_user = local_user_view.map(|l| l.local_user);
let is_admin = local_user.clone().map(|l| l.admin).unwrap_or_default();
match object { match object {
SearchableObjects::PostOrComment(pc) => match *pc { SearchableObjects::PostOrComment(pc) => match *pc {
PostOrComment::Post(p) => { PostOrComment::Post(p) => {
removed_or_deleted = p.deleted || p.removed; res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?)
res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?)
} }
PostOrComment::Comment(c) => { PostOrComment::Comment(c) => {
removed_or_deleted = c.deleted || c.removed;
res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?) res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?)
} }
}, },
SearchableObjects::PersonOrCommunity(pc) => match *pc { SearchableObjects::PersonOrCommunity(pc) => match *pc {
UserOrCommunity::User(u) => { UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?),
removed_or_deleted = u.deleted;
res.person = Some(PersonView::read(pool, u.id).await?)
}
UserOrCommunity::Community(c) => { UserOrCommunity::Community(c) => {
removed_or_deleted = c.deleted || c.removed; res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?)
res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), false).await?)
} }
}, },
}; };
// if the object was deleted from database, dont return it
if removed_or_deleted {
Err(NotFound {}.into())
} else {
Ok(Json(res)) Ok(Json(res))
} }
#[cfg(test)]
mod tests {
use crate::api::resolve_object::resolve_object;
use actix_web::web::Query;
use lemmy_api_common::{context::LemmyContext, site::ResolveObject};
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
local_site::{LocalSite, LocalSiteInsertForm},
post::{Post, PostInsertForm, PostUpdateForm},
site::{Site, SiteInsertForm},
},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use serial_test::serial;
#[tokio::test]
#[serial]
#[expect(clippy::unwrap_used)]
async fn test_object_visibility() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let name = "test_local_user_name";
let bio = "test_local_user_bio";
let creator = LocalUserView::create_test_user(pool, name, bio, false).await?;
let regular_user = LocalUserView::create_test_user(pool, name, bio, false).await?;
let admin_user = LocalUserView::create_test_user(pool, name, bio, true).await?;
let instance_id = creator.person.instance_id;
let site_form = SiteInsertForm::new("test site".to_string(), instance_id);
let site = Site::create(pool, &site_form).await?;
let local_site_form = LocalSiteInsertForm {
site_setup: Some(true),
private_instance: Some(false),
..LocalSiteInsertForm::new(site.id)
};
LocalSite::create(pool, &local_site_form).await?;
let community = Community::create(
pool,
&CommunityInsertForm::new(
instance_id,
"test".to_string(),
"test".to_string(),
"pubkey".to_string(),
),
)
.await?;
let post_insert_form = PostInsertForm::new("Test".to_string(), creator.person.id, community.id);
let post = Post::create(pool, &post_insert_form).await?;
let query = format!("q={}", post.ap_id).to_string();
let query: Query<ResolveObject> = Query::from_query(&query)?;
// Objects should be resolvable without authentication
let res = resolve_object(query.clone(), context.reset_request_count(), None).await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
// Objects should be resolvable by regular users
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(regular_user.clone()),
)
.await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
// Objects should be resolvable by admins
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(admin_user.clone()),
)
.await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
Post::update(
pool,
post.id,
&PostUpdateForm {
deleted: Some(true),
..Default::default()
},
)
.await?;
// Deleted objects should not be resolvable without authentication
let res = resolve_object(query.clone(), context.reset_request_count(), None).await;
assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound));
// Deleted objects should not be resolvable by regular users
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(regular_user.clone()),
)
.await;
assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound));
// Deleted objects should be resolvable by admins
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(admin_user.clone()),
)
.await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
LocalSite::delete(pool).await?;
Site::delete(pool, site.id).await?;
Instance::delete(pool, instance_id).await?;
Ok(())
}
} }

View file

@ -314,17 +314,13 @@ where
#[cfg(test)] #[cfg(test)]
#[expect(clippy::indexing_slicing)] #[expect(clippy::indexing_slicing)]
pub(crate) mod tests { pub(crate) mod tests {
use crate::api::user_settings_backup::{export_settings, import_settings}; use crate::api::user_settings_backup::{export_settings, import_settings};
use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::context::LemmyContext; use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm}, community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm},
instance::Instance, local_user::LocalUser,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
}, },
traits::{Crud, Followable}, traits::{Crud, Followable},
}; };
@ -336,32 +332,13 @@ pub(crate) mod tests {
use std::time::Duration; use std::time::Duration;
use tokio::time::sleep; use tokio::time::sleep;
pub(crate) async fn create_user(
name: String,
bio: Option<String>,
context: &Data<LemmyContext>,
) -> LemmyResult<LocalUserView> {
let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?;
let person_form = PersonInsertForm {
display_name: Some(name.clone()),
bio,
..PersonInsertForm::test_form(instance.id, &name)
};
let person = Person::create(&mut context.pool(), &person_form).await?;
let user_form = LocalUserInsertForm::test_form(person.id);
let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?;
Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?)
}
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_settings_export_import() -> LemmyResult<()> { async fn test_settings_export_import() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let export_user = let export_user = LocalUserView::create_test_user(pool, "hanna", "my bio", false).await?;
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let community_form = CommunityInsertForm::new( let community_form = CommunityInsertForm::new(
export_user.person.instance_id, export_user.person.instance_id,
@ -369,25 +346,25 @@ pub(crate) mod tests {
"testcom".to_string(), "testcom".to_string(),
"pubkey".to_string(), "pubkey".to_string(),
); );
let community = Community::create(&mut context.pool(), &community_form).await?; let community = Community::create(pool, &community_form).await?;
let follower_form = CommunityFollowerForm { let follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
person_id: export_user.person.id, person_id: export_user.person.id,
pending: false, pending: false,
}; };
CommunityFollower::follow(&mut context.pool(), &follower_form).await?; CommunityFollower::follow(pool, &follower_form).await?;
let backup = export_settings(export_user.clone(), context.reset_request_count()).await?; let backup = export_settings(export_user.clone(), context.reset_request_count()).await?;
let import_user = create_user("charles".to_string(), None, &context).await?; let import_user =
LocalUserView::create_test_user(pool, "charles", "charles bio", false).await?;
import_settings(backup, import_user.clone(), context.reset_request_count()).await?; import_settings(backup, import_user.clone(), context.reset_request_count()).await?;
// wait for background task to finish // wait for background task to finish
sleep(Duration::from_millis(1000)).await; sleep(Duration::from_millis(1000)).await;
let import_user_updated = let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?;
LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?;
assert_eq!( assert_eq!(
export_user.person.display_name, export_user.person.display_name,
@ -395,13 +372,12 @@ pub(crate) mod tests {
); );
assert_eq!(export_user.person.bio, import_user_updated.person.bio); assert_eq!(export_user.person.bio, import_user_updated.person.bio);
let follows = let follows = CommunityFollowerView::for_person(pool, import_user.person.id).await?;
CommunityFollowerView::for_person(&mut context.pool(), import_user.person.id).await?;
assert_eq!(follows.len(), 1); assert_eq!(follows.len(), 1);
assert_eq!(follows[0].community.actor_id, community.actor_id); assert_eq!(follows[0].community.actor_id, community.actor_id);
LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?; LocalUser::delete(pool, export_user.local_user.id).await?;
LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; LocalUser::delete(pool, import_user.local_user.id).await?;
Ok(()) Ok(())
} }
@ -409,9 +385,9 @@ pub(crate) mod tests {
#[serial] #[serial]
async fn disallow_large_backup() -> LemmyResult<()> { async fn disallow_large_backup() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let export_user = let export_user = LocalUserView::create_test_user(pool, "harry", "harry bio", false).await?;
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let mut backup = export_settings(export_user.clone(), context.reset_request_count()).await?; let mut backup = export_settings(export_user.clone(), context.reset_request_count()).await?;
@ -426,7 +402,7 @@ pub(crate) mod tests {
backup.saved_comments.push("http://example4.com".parse()?); backup.saved_comments.push("http://example4.com".parse()?);
} }
let import_user = create_user("charles".to_string(), None, &context).await?; let import_user = LocalUserView::create_test_user(pool, "sally", "sally bio", false).await?;
let imported = let imported =
import_settings(backup, import_user.clone(), context.reset_request_count()).await; import_settings(backup, import_user.clone(), context.reset_request_count()).await;
@ -436,8 +412,8 @@ pub(crate) mod tests {
Some(LemmyErrorType::TooManyItems) Some(LemmyErrorType::TooManyItems)
); );
LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?; LocalUser::delete(pool, export_user.local_user.id).await?;
LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?; LocalUser::delete(pool, import_user.local_user.id).await?;
Ok(()) Ok(())
} }
@ -445,9 +421,9 @@ pub(crate) mod tests {
#[serial] #[serial]
async fn import_partial_backup() -> LemmyResult<()> { async fn import_partial_backup() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let import_user = let import_user = LocalUserView::create_test_user(pool, "larry", "larry bio", false).await?;
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let backup = let backup =
serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?; serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?;
@ -458,8 +434,7 @@ pub(crate) mod tests {
) )
.await?; .await?;
let import_user_updated = let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?;
LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?;
// mark as bot account // mark as bot account
assert!(import_user_updated.person.bot_account); assert!(import_user_updated.person.bot_account);
// dont remove existing bio // dont remove existing bio

View file

@ -104,7 +104,6 @@ async fn format_actor_url(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::api::user_settings_backup::tests::create_user;
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityInsertForm}, community::{Community, CommunityInsertForm},
@ -112,6 +111,7 @@ mod tests {
}, },
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serial_test::serial; use serial_test::serial;
@ -130,7 +130,8 @@ mod tests {
), ),
) )
.await?; .await?;
let user = create_user("john".to_string(), None, &context).await?; let user =
LocalUserView::create_test_user(&mut context.pool(), "garda", "garda bio", false).await?;
// insert a remote post which is already fetched // insert a remote post which is already fetched
let post_form = PostInsertForm { let post_form = PostInsertForm {

View file

@ -5,6 +5,12 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{LocalUserId, OAuthProviderId, PersonId}, newtypes::{LocalUserId, OAuthProviderId, PersonId},
schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates}, schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates},
source::{
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
},
traits::Crud,
utils::{ utils::{
functions::{coalesce, lower}, functions::{coalesce, lower},
DbConn, DbConn,
@ -134,6 +140,31 @@ impl LocalUserView {
pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> { pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
queries().list(pool, ListMode::AdminsWithEmails).await queries().list(pool, ListMode::AdminsWithEmails).await
} }
pub async fn create_test_user(
pool: &mut DbPool<'_>,
name: &str,
bio: &str,
admin: bool,
) -> Result<Self, Error> {
let instance_id = Instance::read_or_create(pool, "example.com".to_string())
.await?
.id;
let person_form = PersonInsertForm {
display_name: Some(name.to_owned()),
bio: Some(bio.to_owned()),
..PersonInsertForm::test_form(instance_id, name)
};
let person = Person::create(pool, &person_form).await?;
let user_form = match admin {
true => LocalUserInsertForm::test_form_admin(person.id),
false => LocalUserInsertForm::test_form(person.id),
};
let local_user = LocalUser::create(pool, &user_form, vec![]).await?;
LocalUserView::read(pool, local_user.id).await
}
} }
impl FromRequest for LocalUserView { impl FromRequest for LocalUserView {